[BeatsCM] Beat config status added and other bug fixes (#24246)
* fix spelling * Adds config status feature, fixes a number of other bugs * fix typos and move beat status to OK, ERROR, and UNKNOWN * tweak security again * Fix typo * remove invalid expect * Removed cruft * add missing field to template, renamed said field * tweaks * Update x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js Co-Authored-By: mattapperson <me@mattapperson.com>
This commit is contained in:
parent
ee334c4555
commit
a353979ebb
|
@ -57,6 +57,7 @@ export interface ReturnedConfigurationBlock
|
|||
|
||||
export interface CMBeat {
|
||||
id: string;
|
||||
config_status: 'OK' | 'UNKNOWN' | 'ERROR';
|
||||
enrollment_token: string;
|
||||
active: boolean;
|
||||
access_token: string;
|
||||
|
@ -66,7 +67,7 @@ export interface CMBeat {
|
|||
host_ip: string;
|
||||
host_name: string;
|
||||
ephemeral_id?: string;
|
||||
last_updated?: string;
|
||||
last_checkin?: Date;
|
||||
event_rate?: string;
|
||||
local_configuration_yml?: string;
|
||||
tags?: string[];
|
||||
|
|
|
@ -78,6 +78,7 @@ class FieldText extends Component<
|
|||
className,
|
||||
disabled,
|
||||
helpText,
|
||||
placeholder,
|
||||
} = this.props;
|
||||
|
||||
const { allowError } = this.state;
|
||||
|
@ -94,6 +95,7 @@ class FieldText extends Component<
|
|||
<EuiFieldText
|
||||
id={id}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={getValue() || ''}
|
||||
isInvalid={!disabled && error}
|
||||
onChange={this.handleChange}
|
||||
|
|
|
@ -80,6 +80,7 @@ class MultiFieldText extends Component<
|
|||
className,
|
||||
disabled,
|
||||
helpText,
|
||||
placeholder,
|
||||
} = this.props;
|
||||
|
||||
const { allowError } = this.state;
|
||||
|
@ -98,6 +99,7 @@ class MultiFieldText extends Component<
|
|||
name={name}
|
||||
value={getValue() ? getValue().join('\n') : ''}
|
||||
isInvalid={!disabled && error}
|
||||
placeholder={placeholder}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
fullWidth={fullWidth}
|
||||
|
|
|
@ -61,10 +61,10 @@ export const tagConfigAssignmentOptions: AssignmentControlSchema[] = [
|
|||
type: AssignmentComponentType.Action,
|
||||
danger: true,
|
||||
grow: false,
|
||||
name: 'Detach beat(s)',
|
||||
name: 'Remove tag(s)',
|
||||
showWarning: true,
|
||||
warningHeading: 'Detatch beats',
|
||||
warningMessage: 'This will detatch the selected beat(s) from this tag.',
|
||||
warningHeading: 'Remove tag(s)',
|
||||
warningMessage: 'Remove the tag from the selected beat(s)?',
|
||||
action: AssignmentActionType.Delete,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiToolTip, IconColor } from '@elastic/eui';
|
||||
import { first, sortBy, sortByOrder, uniq } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
@ -83,12 +83,41 @@ export const BeatsTableType: TableType = {
|
|||
},
|
||||
{
|
||||
// TODO: update to use actual metadata field
|
||||
field: 'event_rate',
|
||||
name: 'Event rate',
|
||||
sortable: true,
|
||||
field: 'config_status',
|
||||
name: 'Config Status',
|
||||
render: (value: string, beat: CMPopulatedBeat) => {
|
||||
let color: IconColor = 'success';
|
||||
let statusText = 'OK';
|
||||
let tooltipText = 'Beat successfully applied latest config';
|
||||
|
||||
switch (beat.config_status) {
|
||||
case 'UNKNOWN':
|
||||
color = 'subdued';
|
||||
statusText = 'Offline';
|
||||
if (moment().diff(beat.last_checkin, 'minutes') >= 10) {
|
||||
tooltipText = 'This Beat has not connected to kibana in over 10min';
|
||||
} else {
|
||||
tooltipText = 'This Beat has not yet been started.';
|
||||
}
|
||||
break;
|
||||
case 'ERROR':
|
||||
color = 'danger';
|
||||
statusText = 'Error';
|
||||
tooltipText = 'Please check the logs of this Beat for error details';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
|
||||
<EuiToolTip content={tooltipText}>
|
||||
<EuiHealth color={color}>{statusText}</EuiHealth>
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
// TODO: update to use actual metadata field
|
||||
field: 'full_tags',
|
||||
name: 'Last config update',
|
||||
render: (tags: BeatTag[]) =>
|
||||
|
|
|
@ -74,8 +74,8 @@ interface ComponentProps {
|
|||
values: ConfigurationBlock;
|
||||
schema: YamlConfigSchema[];
|
||||
id: string;
|
||||
onSubmit?: (modal: any) => any;
|
||||
canSubmit(canIt: boolean): any;
|
||||
onSubmit(modal: any): any;
|
||||
}
|
||||
|
||||
export class ConfigForm extends React.Component<ComponentProps, any> {
|
||||
|
@ -101,11 +101,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
this.props.canSubmit(false);
|
||||
};
|
||||
public submit = () => {
|
||||
if (this.form.current) {
|
||||
if (this.form.current && this.props.onSubmit) {
|
||||
this.form.current.click();
|
||||
}
|
||||
};
|
||||
public onValidSubmit = <ModelType extends any>(model: ModelType) => {
|
||||
if (!this.props.onSubmit) {
|
||||
return;
|
||||
}
|
||||
const processed = JSON.parse(JSON.stringify(model), (key, value) => {
|
||||
return _.isObject(value) && !_.isArray(value)
|
||||
? _.mapKeys(value, (v, k: string) => {
|
||||
|
@ -139,9 +142,15 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
<FormsyEuiFieldText
|
||||
key={schema.id}
|
||||
id={schema.id}
|
||||
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
|
||||
defaultValue={get(
|
||||
this.props,
|
||||
`values.configs[0].${schema.id}`,
|
||||
schema.defaultValue
|
||||
)}
|
||||
name={schema.id}
|
||||
disabled={!this.props.onSubmit}
|
||||
helpText={schema.ui.helpText}
|
||||
placeholder={schema.ui.placeholder}
|
||||
label={schema.ui.label}
|
||||
validations={schema.validations}
|
||||
validationError={schema.error}
|
||||
|
@ -153,8 +162,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
<FormsyEuiPasswordText
|
||||
key={schema.id}
|
||||
id={schema.id}
|
||||
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
|
||||
disabled={!this.props.onSubmit}
|
||||
defaultValue={get(
|
||||
this.props,
|
||||
`values.configs[0].${schema.id}`,
|
||||
schema.defaultValue
|
||||
)}
|
||||
name={schema.id}
|
||||
placeholder={schema.ui.placeholder}
|
||||
helpText={schema.ui.helpText}
|
||||
label={schema.ui.label}
|
||||
validations={schema.validations}
|
||||
|
@ -167,8 +182,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
<FormsyEuiMultiFieldText
|
||||
key={schema.id}
|
||||
id={schema.id}
|
||||
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
|
||||
disabled={!this.props.onSubmit}
|
||||
defaultValue={get(
|
||||
this.props,
|
||||
`values.configs[0].${schema.id}`,
|
||||
schema.defaultValue
|
||||
)}
|
||||
name={schema.id}
|
||||
placeholder={schema.ui.placeholder}
|
||||
helpText={schema.ui.helpText}
|
||||
label={schema.ui.label}
|
||||
validations={schema.validations}
|
||||
|
@ -182,7 +203,12 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
key={schema.id}
|
||||
id={schema.id}
|
||||
name={schema.id}
|
||||
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
|
||||
disabled={!this.props.onSubmit}
|
||||
defaultValue={get(
|
||||
this.props,
|
||||
`values.configs[0].${schema.id}`,
|
||||
schema.defaultValue
|
||||
)}
|
||||
helpText={schema.ui.helpText}
|
||||
label={schema.ui.label}
|
||||
options={[{ value: '', text: 'Please Select An Option' }].concat(
|
||||
|
@ -198,8 +224,13 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
<FormsyEuiCodeEditor
|
||||
key={`${schema.id}-${this.props.id}`}
|
||||
mode="yaml"
|
||||
disabled={!this.props.onSubmit}
|
||||
id={schema.id}
|
||||
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
|
||||
defaultValue={get(
|
||||
this.props,
|
||||
`values.configs[0].${schema.id}`,
|
||||
schema.defaultValue
|
||||
)}
|
||||
name={schema.id}
|
||||
helpText={schema.ui.helpText}
|
||||
label={schema.ui.label}
|
||||
|
@ -211,12 +242,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
|
|||
);
|
||||
}
|
||||
})}
|
||||
<button
|
||||
type="submit"
|
||||
style={{ display: 'none' }}
|
||||
disabled={!this.state.canSubmit}
|
||||
ref={this.form}
|
||||
/>
|
||||
{this.props.onSubmit && (
|
||||
<button
|
||||
type="submit"
|
||||
style={{ display: 'none' }}
|
||||
disabled={!this.state.canSubmit}
|
||||
ref={this.form}
|
||||
/>
|
||||
)}
|
||||
</Formsy>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ import { ConfigForm } from './config_form';
|
|||
interface ComponentProps {
|
||||
configBlock?: ConfigurationBlock;
|
||||
onClose(): any;
|
||||
onSave(config: ConfigurationBlock): any;
|
||||
onSave?(config: ConfigurationBlock): any;
|
||||
}
|
||||
|
||||
export class ConfigView extends React.Component<ComponentProps, any> {
|
||||
|
@ -66,11 +66,17 @@ export class ConfigView extends React.Component<ComponentProps, any> {
|
|||
<EuiFlyout onClose={this.props.onClose}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>{this.editMode ? 'Edit Configuration' : 'Add Configuration'}</h2>
|
||||
<h2>
|
||||
{this.editMode
|
||||
? this.props.onSave
|
||||
? 'Edit configuration'
|
||||
: 'View configuration'
|
||||
: 'Add configuration'}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiFormRow label="Configuration type">
|
||||
<EuiFormRow label="Type">
|
||||
<EuiSelect
|
||||
options={supportedConfigs}
|
||||
value={this.state.configBlock.type}
|
||||
|
@ -78,31 +84,37 @@ export class ConfigView extends React.Component<ComponentProps, any> {
|
|||
onChange={this.onValueChange('type')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow label="Configuration description">
|
||||
<EuiFormRow label="Description">
|
||||
<EuiFieldText
|
||||
value={this.state.configBlock.description}
|
||||
disabled={!this.props.onSave}
|
||||
onChange={this.onValueChange('description')}
|
||||
placeholder="Description (optional)"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<h3>
|
||||
Config for
|
||||
{
|
||||
(supportedConfigs.find(config => this.state.configBlock.type === config.value) as any)
|
||||
.text
|
||||
}
|
||||
configuration
|
||||
</h3>
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<ConfigForm
|
||||
// tslint:disable-next-line:no-console
|
||||
onSubmit={data => {
|
||||
this.props.onSave({
|
||||
...this.state.configBlock,
|
||||
configs: [data],
|
||||
});
|
||||
this.props.onClose();
|
||||
}}
|
||||
onSubmit={
|
||||
this.props.onSave
|
||||
? data => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave({
|
||||
...this.state.configBlock,
|
||||
configs: [data],
|
||||
});
|
||||
}
|
||||
this.props.onClose();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canSubmit={canIt => this.setState({ valid: canIt })}
|
||||
ref={this.form}
|
||||
values={this.state.configBlock}
|
||||
|
@ -123,19 +135,21 @@ export class ConfigView extends React.Component<ComponentProps, any> {
|
|||
Close
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
disabled={!this.state.valid}
|
||||
fill
|
||||
onClick={() => {
|
||||
if (this.form.current) {
|
||||
this.form.current.submit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{this.props.onSave && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
disabled={!this.state.valid}
|
||||
fill
|
||||
onClick={() => {
|
||||
if (this.form.current) {
|
||||
this.form.current.submit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -65,11 +65,7 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
<h3>Tag details</h3>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
Tags will apply the configurations below to all beats assigned this tag.
|
||||
<br />
|
||||
The tag type defines the options available.
|
||||
</p>
|
||||
<p>Tags will apply the configurations below to all beats assigned this tag.</p>
|
||||
</EuiText>
|
||||
<div>
|
||||
<TagBadge tag={{ color: tag.color || '#FF0', id: tag.id }} />
|
||||
|
@ -113,9 +109,8 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
</EuiTitle>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
Tags can contain multiple configurations. These configurations can repeat or mix
|
||||
types as necessary. For example, you may utilize three metricbeat configurations
|
||||
alongside one input and filebeat configuration.
|
||||
Tags can have configurations for different types of Beats. For example, a tag can
|
||||
have two Metricbeat configurations and one Filebeat input configuration.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
@ -156,7 +151,7 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
<EuiHorizontalRule />
|
||||
|
||||
<EuiTitle size="xs">
|
||||
<h3>Attached Beats</h3>
|
||||
<h3>Beats assigned this tag</h3>
|
||||
</EuiTitle>
|
||||
<Table
|
||||
assignmentOptions={{
|
||||
|
|
|
@ -12,6 +12,8 @@ const filebeatInputConfig: YamlConfigSchema[] = [
|
|||
ui: {
|
||||
label: 'Paths',
|
||||
type: 'multi-input',
|
||||
helpText: 'Put each of the paths on a seperate line',
|
||||
placeholder: `first/path/to/file.json second/path/to/otherfile.json`,
|
||||
},
|
||||
validations: 'isPaths',
|
||||
error: 'One file path per line',
|
||||
|
@ -22,9 +24,10 @@ const filebeatInputConfig: YamlConfigSchema[] = [
|
|||
ui: {
|
||||
label: 'Other Config',
|
||||
type: 'code',
|
||||
helpText: 'Use YAML format to specify other settings for the Filebeat Input',
|
||||
},
|
||||
validations: 'isYaml',
|
||||
error: 'Config entered must be in valid YAML format',
|
||||
error: 'Use valid YAML format',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -113,9 +116,10 @@ const filebeatModuleConfig: YamlConfigSchema[] = [
|
|||
ui: {
|
||||
label: 'Other Config',
|
||||
type: 'code',
|
||||
helpText: 'Use YAML format to specify other settings for the Filebeat Module',
|
||||
},
|
||||
validations: 'isYaml',
|
||||
error: 'Config entered must be in valid YAML format',
|
||||
error: 'Use valid YAML format',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -276,6 +280,8 @@ const metricbeatModuleConfig: YamlConfigSchema[] = [
|
|||
ui: {
|
||||
label: 'Hosts',
|
||||
type: 'multi-input',
|
||||
helpText: 'Put each of the paths on a seperate line',
|
||||
placeholder: `somehost.local otherhost.local`,
|
||||
},
|
||||
validations: 'isHosts',
|
||||
error: 'One file host per line',
|
||||
|
@ -297,9 +303,10 @@ const metricbeatModuleConfig: YamlConfigSchema[] = [
|
|||
ui: {
|
||||
label: 'Other Config',
|
||||
type: 'code',
|
||||
helpText: 'Use YAML format to specify other settings for the Metricbeat Module',
|
||||
},
|
||||
validations: 'isYaml',
|
||||
error: 'Config entered must be in valid YAML format',
|
||||
error: 'Use valid YAML format',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -54,9 +54,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
|||
};
|
||||
|
||||
public render = (component: React.ReactElement<any>) => {
|
||||
if (this.hadValidLicense() && this.securityEnabled()) {
|
||||
this.rootComponent = component;
|
||||
}
|
||||
this.rootComponent = component;
|
||||
};
|
||||
|
||||
public hadValidLicense() {
|
||||
|
@ -75,10 +73,10 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
|||
}
|
||||
|
||||
public registerManagementSection(pluginId: string, displayName: string, basePath: string) {
|
||||
if (this.hadValidLicense() && this.securityEnabled()) {
|
||||
this.register(this.uiModule);
|
||||
this.register(this.uiModule);
|
||||
|
||||
this.hookAngular(() => {
|
||||
this.hookAngular(() => {
|
||||
if (this.hadValidLicense() && this.securityEnabled()) {
|
||||
const registerSection = () =>
|
||||
this.management.register(pluginId, {
|
||||
display: 'Beats', // TODO these need to be config options not hard coded in the adapter
|
||||
|
@ -95,8 +93,8 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
|||
order: 30,
|
||||
url: `#${basePath}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private manageAngularLifecycle($scope: any, $route: any, elem: any) {
|
||||
|
|
|
@ -29,6 +29,7 @@ export interface YamlConfigSchema {
|
|||
label: string;
|
||||
type: 'input' | 'multi-input' | 'select' | 'code' | 'password';
|
||||
helpText?: string;
|
||||
placeholder?: string;
|
||||
transform?: 'removed';
|
||||
};
|
||||
options?: Array<{ value: string; text: string }>;
|
||||
|
|
|
@ -16,88 +16,119 @@ import {
|
|||
import { flatten, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { TABLE_CONFIG } from '../../../common/constants';
|
||||
import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { ConnectedLink } from '../../components/connected_link';
|
||||
import { TagBadge } from '../../components/tag';
|
||||
import { ConfigView } from '../../components/tag/config_view/index';
|
||||
import { supportedConfigs } from '../../config_schemas';
|
||||
|
||||
interface BeatDetailPageProps {
|
||||
interface PageProps {
|
||||
beat: CMPopulatedBeat | undefined;
|
||||
}
|
||||
|
||||
export const BeatDetailPage = (props: BeatDetailPageProps) => {
|
||||
const { beat } = props;
|
||||
if (!beat) {
|
||||
return <div>Beat not found</div>;
|
||||
}
|
||||
const configurationBlocks = flatten(
|
||||
beat.full_tags.map((tag: BeatTag) => {
|
||||
return tag.configuration_blocks.map(configuration => ({
|
||||
// @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property
|
||||
module: configuration.configs[0].module || null,
|
||||
tagId: tag.id,
|
||||
tagColor: tag.color,
|
||||
...beat,
|
||||
...configuration,
|
||||
displayValue: get(
|
||||
supportedConfigs.find(config => config.value === configuration.type),
|
||||
'text',
|
||||
null
|
||||
),
|
||||
}));
|
||||
})
|
||||
);
|
||||
interface PageState {
|
||||
selectedConfig: ConfigurationBlock | null;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'displayValue',
|
||||
name: 'Type',
|
||||
sortable: true,
|
||||
render: (value: string | null, configuration: any) => (
|
||||
<EuiLink href="#">{value || configuration.type}</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'module',
|
||||
name: 'Module',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'tagId',
|
||||
name: 'Tag',
|
||||
render: (id: string, block: any) => (
|
||||
<ConnectedLink path={`/tag/edit/${id}`}>
|
||||
<TagBadge
|
||||
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
|
||||
tag={{ color: block.tagColor, id }}
|
||||
export class BeatDetailPage extends React.PureComponent<PageProps, PageState> {
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedConfig: null,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const props = this.props;
|
||||
const { beat } = props;
|
||||
if (!beat) {
|
||||
return <div>Beat not found</div>;
|
||||
}
|
||||
const configurationBlocks = flatten(
|
||||
beat.full_tags.map((tag: BeatTag) => {
|
||||
return tag.configuration_blocks.map(configuration => ({
|
||||
// @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property
|
||||
module: configuration.configs[0].module || null,
|
||||
tagId: tag.id,
|
||||
tagColor: tag.color,
|
||||
...beat,
|
||||
...configuration,
|
||||
displayValue: get(
|
||||
supportedConfigs.find(config => config.value === configuration.type),
|
||||
'text',
|
||||
null
|
||||
),
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'displayValue',
|
||||
name: 'Type',
|
||||
sortable: true,
|
||||
render: (value: string | null, configuration: any) => (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
selectedConfig: configuration,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{value || configuration.type}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'module',
|
||||
name: 'Module',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'tagId',
|
||||
name: 'Tag',
|
||||
render: (id: string, block: any) => (
|
||||
<ConnectedLink path={`/tag/edit/${id}`}>
|
||||
<TagBadge
|
||||
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
|
||||
tag={{ color: block.tagColor, id }}
|
||||
/>
|
||||
</ConnectedLink>
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>Configurations</h4>
|
||||
</EuiTitle>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
You can have multiple configurations applied to an individual tag. These
|
||||
configurations can repeat or mix types as necessary. For example, you may utilize
|
||||
three metricbeat configurations alongside one input and filebeat configuration.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiInMemoryTable columns={columns} items={configurationBlocks} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{this.state.selectedConfig && (
|
||||
<ConfigView
|
||||
configBlock={this.state.selectedConfig}
|
||||
onClose={() => this.setState({ selectedConfig: null })}
|
||||
/>
|
||||
</ConnectedLink>
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>Configurations</h4>
|
||||
</EuiTitle>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
You can have multiple configurations applied to an individual tag. These configurations
|
||||
can repeat or mix types as necessary. For example, you may utilize three metricbeat
|
||||
configurations alongside one input and filebeat configuration.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiInMemoryTable columns={columns} items={configurationBlocks} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Select your beat type:</h3>
|
||||
<h3>Beat type:</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -140,7 +140,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Select your platform:</h3>
|
||||
<h3>Platform:</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -175,7 +175,10 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Run the following command to enroll your beat</h3>
|
||||
<h3>
|
||||
On the host where your {capitalize(this.state.beatType)} is installed,
|
||||
run:
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -200,7 +203,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>Waiting for enroll command to be run...</h3>
|
||||
<h3>Waiting for {capitalize(this.state.beatType)} to enroll...</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -214,8 +217,8 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
</React.Fragment>
|
||||
)}
|
||||
{this.state.enrolledBeat && (
|
||||
<EuiModalBody style={{ textAlign: 'center' }}>
|
||||
A Beat was enrolled with the following data:
|
||||
<EuiModalBody>
|
||||
The Beat is now enrolled in central management:
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiCode,
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore
|
||||
|
@ -78,7 +77,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
const tabs = [
|
||||
{
|
||||
id: '/overview/beats',
|
||||
name: 'Beats List',
|
||||
name: 'Enrolled Beats',
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
|
@ -88,7 +87,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
// },
|
||||
{
|
||||
id: '/overview/tags',
|
||||
name: 'Configuration Tags',
|
||||
name: 'Configuration tags',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
@ -102,7 +101,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
},
|
||||
{
|
||||
id: '/overview/initial/tag',
|
||||
name: 'Create Configuration Tag',
|
||||
name: 'Create tag',
|
||||
disabled: false,
|
||||
page: CreateTagPageFragment,
|
||||
},
|
||||
|
@ -117,7 +116,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
if (this.props.location.pathname === '/overview/initial/help') {
|
||||
return (
|
||||
<NoDataLayout
|
||||
title="Welcome to Beats Central Management"
|
||||
title="Beats central management"
|
||||
actionSection={
|
||||
<ConnectedLink path="/overview/initial/beats">
|
||||
<EuiButton color="primary" fill>
|
||||
|
@ -126,10 +125,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
</ConnectedLink>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
You don’t have any Beat configured to use Central Management, click on{' '}
|
||||
<EuiCode>Enroll Beat</EuiCode> to add one now.
|
||||
</p>
|
||||
<p>Manage your configurations in a central location.</p>
|
||||
</NoDataLayout>
|
||||
);
|
||||
}
|
||||
|
@ -137,7 +133,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
|
|||
if (this.props.location.pathname.includes('/overview/initial')) {
|
||||
return (
|
||||
<WalkthroughLayout
|
||||
title="Get Started With Beats Centeral Management"
|
||||
title="Get started with Beats central management"
|
||||
walkthroughSteps={walkthroughSteps}
|
||||
goTo={this.props.goTo}
|
||||
activePath={this.props.location.pathname}
|
||||
|
|
|
@ -36,6 +36,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
|
|||
}
|
||||
|
||||
public async insert(user: FrameworkUser, beat: CMBeat) {
|
||||
beat.config_status = 'UNKNOWN';
|
||||
const body = {
|
||||
beat,
|
||||
type: 'beat',
|
||||
|
@ -230,6 +231,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
|
|||
refresh: 'wait_for',
|
||||
type: '_doc',
|
||||
});
|
||||
// console.log(response.items[0].update.error);
|
||||
return _get<any>(response, 'items', []).map((item: any, resultIdx: any) => ({
|
||||
idxInRequest: assignments[resultIdx].idxInRequest,
|
||||
result: item.update.result,
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('Beats Domain Lib', () => {
|
|||
beatsDB = [
|
||||
{
|
||||
access_token: '9a6c99ae0fd84b068819701169cd8a4b',
|
||||
config_status: 'OK',
|
||||
active: true,
|
||||
enrollment_token: '23423423423',
|
||||
host_ip: '1.2.3.4',
|
||||
|
@ -49,6 +50,7 @@ describe('Beats Domain Lib', () => {
|
|||
{
|
||||
access_token: '188255eb560a4448b72656c5e99cae6f',
|
||||
active: true,
|
||||
config_status: 'OK',
|
||||
enrollment_token: 'reertrte',
|
||||
host_ip: '22.33.11.44',
|
||||
host_name: 'baz.bar.com',
|
||||
|
@ -59,6 +61,7 @@ describe('Beats Domain Lib', () => {
|
|||
access_token: '93c4a4dd08564c189a7ec4e4f046b975',
|
||||
active: true,
|
||||
enrollment_token: '23s423423423',
|
||||
config_status: 'OK',
|
||||
host_ip: '1.2.3.4',
|
||||
host_name: 'foo.bar.com',
|
||||
id: 'foo',
|
||||
|
@ -70,6 +73,7 @@ describe('Beats Domain Lib', () => {
|
|||
access_token: '3c4a4dd08564c189a7ec4e4f046b9759',
|
||||
enrollment_token: 'gdfsgdf',
|
||||
active: true,
|
||||
config_status: 'OK',
|
||||
host_ip: '11.22.33.44',
|
||||
host_name: 'foo.com',
|
||||
id: 'bar',
|
||||
|
|
|
@ -13,7 +13,7 @@ const internalUser: FrameworkInternalUser = { kind: 'internal' };
|
|||
|
||||
describe('Beats Domain Lib', () => {
|
||||
let libs: CMServerLibs;
|
||||
let beatsDB: CMBeat[] = [];
|
||||
let beatsDB: Array<Partial<CMBeat>> = [];
|
||||
let tagsDB: BeatTag[] = [];
|
||||
|
||||
describe('remove_tags_from_beats', () => {
|
||||
|
|
|
@ -74,6 +74,7 @@ describe('Beats Domain lib', () => {
|
|||
it('should return an invalid message if token validation fails', async () => {
|
||||
const beatToFind: CMBeat = {
|
||||
id: beatId,
|
||||
config_status: 'OK',
|
||||
enrollment_token: '',
|
||||
active: true,
|
||||
access_token: token.token || '',
|
||||
|
@ -93,6 +94,7 @@ describe('Beats Domain lib', () => {
|
|||
it('should update the beat when a valid token is provided', async () => {
|
||||
const beatToFind: CMBeat = {
|
||||
id: beatId,
|
||||
config_status: 'OK',
|
||||
enrollment_token: '',
|
||||
active: true,
|
||||
access_token: token.token || '',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import Joi from 'joi';
|
||||
import { omit } from 'lodash';
|
||||
import { BeatTag, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { CMServerLibs } from '../../lib/lib';
|
||||
import { wrapEsError } from '../../utils/error_wrappers';
|
||||
import { ReturnedConfigurationBlock } from './../../../common/domain_types';
|
||||
|
@ -18,6 +18,9 @@ export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({
|
|||
headers: Joi.object({
|
||||
'kbn-beats-access-token': Joi.string().required(),
|
||||
}).options({ allowUnknown: true }),
|
||||
query: Joi.object({
|
||||
validSetting: Joi.boolean().default(true),
|
||||
}),
|
||||
},
|
||||
auth: false,
|
||||
},
|
||||
|
@ -38,6 +41,16 @@ export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({
|
|||
return reply({ message: 'Invalid access token' }).code(401);
|
||||
}
|
||||
|
||||
let newStatus: CMBeat['config_status'] = 'OK';
|
||||
if (!request.query.validSetting) {
|
||||
newStatus = 'ERROR';
|
||||
}
|
||||
|
||||
await libs.beats.update(libs.framework.internalUser, beat.id, {
|
||||
config_status: newStatus,
|
||||
last_checkin: new Date(),
|
||||
});
|
||||
|
||||
tags = await libs.tags.getTagsWithIds(libs.framework.internalUser, beat.tags || []);
|
||||
} catch (err) {
|
||||
return reply(wrapEsError(err));
|
||||
|
|
|
@ -58,9 +58,15 @@
|
|||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"config_status": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"last_checkin": {
|
||||
"type": "date"
|
||||
},
|
||||
"enrollment_token": {
|
||||
"type": "keyword"
|
||||
},
|
||||
|
|
|
@ -69,6 +69,7 @@ export default function ({ getService }) {
|
|||
|
||||
expect(esResponse._source.beat).to.have.property('verified_on');
|
||||
expect(esResponse._source.beat).to.have.property('host_ip');
|
||||
expect(esResponse._source.beat.config_status).to.eql('UNKNOWN');
|
||||
});
|
||||
|
||||
it('should contain an access token in the response', async () => {
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('get_beat_configuration', () => {
|
||||
const archive = 'beats/list';
|
||||
|
@ -47,5 +49,47 @@ export default function ({ getService }) {
|
|||
'node.namespace': 'node',
|
||||
});
|
||||
});
|
||||
|
||||
it('A config check with a validSetting of false should set an error status on the beat', async () => {
|
||||
// 1. Initial request
|
||||
await supertest
|
||||
.get('/api/beats/agent/foo/configuration?validSetting=true')
|
||||
.set(
|
||||
'kbn-beats-access-token',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
|
||||
'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' +
|
||||
'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
let esResponse = await es.get({
|
||||
index: ES_INDEX_NAME,
|
||||
type: ES_TYPE_NAME,
|
||||
id: `beat:foo`,
|
||||
});
|
||||
|
||||
let beat = esResponse._source.beat;
|
||||
expect(beat.config_status).to.be('OK');
|
||||
|
||||
// 2. Beat polls reporting an error
|
||||
await supertest
|
||||
.get('/api/beats/agent/foo/configuration?validSetting=false')
|
||||
.set(
|
||||
'kbn-beats-access-token',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
|
||||
'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' +
|
||||
'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
esResponse = await es.get({
|
||||
index: ES_INDEX_NAME,
|
||||
type: ES_TYPE_NAME,
|
||||
id: `beat:foo`,
|
||||
});
|
||||
|
||||
beat = esResponse._source.beat;
|
||||
expect(beat.config_status).to.be('ERROR');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue