mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [BeatsCM] Cleanup and refactor (#26636)
* Refactor BeatsCM
* update deps
* update more deps
* update for new EUI definitions
* update import
* Revert "update deps"
This reverts commit 759a14561d
.
* use _source_includes
* remove _source_includes
* work-around due to watcher UI tests
* Keep all xpack checks safe because we cant trust its there in tests for some reason
* VALIDATION. This commit is to ensure the errors in CI are coming from beats
* remove validation that this is a beats CM issue
* More try/catch to try and find where this error is
* testing another call
* revert back to dangerouslyGetActiveInjector
* ensure expire always is a number
* fix swallowed error
* Update x-pack/plugins/beats_management/public/lib/compose/kibana.ts
Co-Authored-By: mattapperson <me@mattapperson.com>
* Update x-pack/plugins/beats_management/public/utils/page_loader.test.ts
Co-Authored-By: mattapperson <me@mattapperson.com>
* Update x-pack/plugins/beats_management/public/utils/page_loader.ts
Co-Authored-By: mattapperson <me@mattapperson.com>
* fix for new webpack import
* Fix translation map
* fix URL path
* fix other link
* removing tag from beats via tag details screen now uses container
* remove debug text
* added comment/readme about routing on the client side
* enrolled beat UI now works on overview screen
* newly enrolled beat now reloads the beats table
* fix TS errors
# Conflicts:
# x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts
# x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts
# x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts
* fix import for 6.x
* fix formatting
This commit is contained in:
parent
ebdba853e1
commit
80d9fe5bb7
121 changed files with 2819 additions and 2645 deletions
|
@ -123,8 +123,8 @@
|
|||
"@elastic/numeral": "2.3.2",
|
||||
"@kbn/babel-preset": "1.0.0",
|
||||
"@kbn/i18n": "1.0.0",
|
||||
"@kbn/ui-framework": "1.0.0",
|
||||
"@kbn/interpreter": "1.0.0",
|
||||
"@kbn/ui-framework": "1.0.0",
|
||||
"@samverschueren/stream-to-observable": "^0.3.0",
|
||||
"@scant/router": "^0.1.0",
|
||||
"@slack/client": "^4.8.0",
|
||||
|
@ -179,6 +179,7 @@
|
|||
"icalendar": "0.7.1",
|
||||
"inline-style": "^2.0.0",
|
||||
"intl": "^1.2.5",
|
||||
"io-ts": "^1.4.2",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"joi": "^13.5.2",
|
||||
"jquery": "^3.3.1",
|
||||
|
@ -257,6 +258,7 @@
|
|||
"typescript-fsa-reducers": "^0.4.5",
|
||||
"ui-select": "0.19.4",
|
||||
"unbzip2-stream": "1.0.9",
|
||||
"unstated": "^2.1.1",
|
||||
"uuid": "3.0.1",
|
||||
"venn.js": "0.2.9",
|
||||
"xregexp": "3.2.0"
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
export { PLUGIN } from './plugin';
|
||||
export { INDEX_NAMES } from './index_names';
|
||||
export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks';
|
||||
export const BASE_PATH = '/management/beats_management/';
|
||||
export const BASE_PATH = '/management/beats_management';
|
||||
export { TABLE_CONFIG } from './table';
|
||||
export { REQUIRED_ROLES, LICENSES, REQUIRED_LICENSES } from './security';
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
export const PLUGIN = {
|
||||
ID: 'beats_management',
|
||||
};
|
||||
export const CONFIG_PREFIX = 'xpack.beats';
|
||||
|
|
10
x-pack/plugins/beats_management/common/constants/security.ts
Normal file
10
x-pack/plugins/beats_management/common/constants/security.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const REQUIRED_ROLES = ['beats_admin'];
|
||||
export const REQUIRED_LICENSES = ['standard', 'gold', 'trial', 'platinum'];
|
||||
export const LICENSES = ['oss', 'standard', 'gold', 'trial', 'platinum'];
|
||||
export type LicenseType = 'oss' | 'trial' | 'standard' | 'basic' | 'gold' | 'platinum';
|
|
@ -6,23 +6,24 @@
|
|||
import Joi from 'joi';
|
||||
import { resolve } from 'path';
|
||||
import { PLUGIN } from './common/constants';
|
||||
import { CONFIG_PREFIX } from './common/constants/plugin';
|
||||
import { initServerWithKibana } from './server/kibana.index';
|
||||
import { KibanaLegacyServer } from './server/lib/adapters/framework/adapter_types';
|
||||
|
||||
const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes
|
||||
|
||||
export const config = Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
encryptionKey: Joi.string(),
|
||||
defaultUserRoles: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(['superuser']),
|
||||
encryptionKey: Joi.string().default('xpack_beats_default_encryptionKey'),
|
||||
enrollmentTokensTtlInSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(10 * 60 * 14) // No more then 2 weeks for security reasons
|
||||
.default(DEFAULT_ENROLLMENT_TOKENS_TTL_S),
|
||||
}).default();
|
||||
export const configPrefix = 'xpack.beats';
|
||||
|
||||
export function beats(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
|
@ -33,8 +34,8 @@ export function beats(kibana: any) {
|
|||
managementSections: ['plugins/beats_management'],
|
||||
},
|
||||
config: () => config,
|
||||
configPrefix,
|
||||
init(server: any) {
|
||||
configPrefix: CONFIG_PREFIX,
|
||||
init(server: KibanaLegacyServer) {
|
||||
initServerWithKibana(server);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
// @ts-ignore typings for EuiBasicTable not present in current version
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
|
@ -15,37 +13,43 @@ import {
|
|||
EuiSelect,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { capitalize } from 'lodash';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { CMBeat } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
import { CMBeat } from '../../common/domain_types';
|
||||
|
||||
interface BeatsProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
|
||||
match: any;
|
||||
libs: FrontendLibs;
|
||||
intl: InjectedIntl;
|
||||
interface ComponentProps {
|
||||
/** Such as kibanas basePath, for use to generate command */
|
||||
frameworkBasePath?: string;
|
||||
enrollmentToken?: string;
|
||||
getBeatWithToken(token: string): Promise<CMBeat | null>;
|
||||
createEnrollmentToken(): Promise<void>;
|
||||
onBeatEnrolled(enrolledBeat: CMBeat): void;
|
||||
}
|
||||
export class EnrollBeat extends React.Component<BeatsProps, any> {
|
||||
|
||||
interface ComponentState {
|
||||
enrolledBeat: CMBeat | null;
|
||||
hasPolledForBeat: boolean;
|
||||
command: string;
|
||||
beatType: string;
|
||||
}
|
||||
|
||||
export class EnrollBeat extends React.Component<ComponentProps, ComponentState> {
|
||||
private pinging = false;
|
||||
constructor(props: BeatsProps) {
|
||||
constructor(props: ComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enrolledBeat: null,
|
||||
hasPolledForBeat: false,
|
||||
command: 'sudo filebeat',
|
||||
beatType: 'filebeat',
|
||||
};
|
||||
}
|
||||
public pingForBeatWithToken = async (
|
||||
libs: FrontendLibs,
|
||||
token: string
|
||||
): Promise<CMBeat | void> => {
|
||||
public pingForBeatWithToken = async (token: string): Promise<CMBeat | void> => {
|
||||
try {
|
||||
const beats = await libs.beats.getBeatWithToken(token);
|
||||
const beats = await this.props.getBeatWithToken(token);
|
||||
|
||||
if (!beats) {
|
||||
throw new Error('no beats');
|
||||
}
|
||||
|
@ -54,69 +58,34 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
if (this.pinging) {
|
||||
const timeout = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||
await timeout(5000);
|
||||
return await this.pingForBeatWithToken(libs, token);
|
||||
return await this.pingForBeatWithToken(token);
|
||||
}
|
||||
}
|
||||
};
|
||||
public async componentDidMount() {
|
||||
if (!this.props.urlState.enrollmentToken) {
|
||||
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
|
||||
this.props.setUrlState({
|
||||
enrollmentToken,
|
||||
});
|
||||
if (!this.props.enrollmentToken) {
|
||||
await this.props.createEnrollmentToken();
|
||||
}
|
||||
}
|
||||
public waitForToken = async (token: string) => {
|
||||
if (this.pinging) {
|
||||
public waitForTokenToEnrollBeat = async () => {
|
||||
if (this.pinging || !this.props.enrollmentToken) {
|
||||
return;
|
||||
}
|
||||
this.pinging = true;
|
||||
const enrolledBeat = (await this.pingForBeatWithToken(this.props.libs, token)) as CMBeat;
|
||||
const enrolledBeat = (await this.pingForBeatWithToken(this.props.enrollmentToken)) as CMBeat;
|
||||
|
||||
this.setState({
|
||||
enrolledBeat,
|
||||
});
|
||||
this.props.onBeatEnrolled(enrolledBeat);
|
||||
this.pinging = false;
|
||||
};
|
||||
public render() {
|
||||
if (!this.props.urlState.enrollmentToken) {
|
||||
if (!this.props.enrollmentToken && !this.state.enrolledBeat) {
|
||||
return null;
|
||||
}
|
||||
if (this.props.urlState.enrollmentToken && !this.state.enrolledBeat) {
|
||||
this.waitForToken(this.props.urlState.enrollmentToken);
|
||||
}
|
||||
const { goTo, intl } = this.props;
|
||||
|
||||
const actions = [];
|
||||
|
||||
switch (this.props.location.pathname) {
|
||||
case '/overview/initial/beats':
|
||||
actions.push({
|
||||
goTo: '/overview/initial/tag',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.continueButtonLabel',
|
||||
defaultMessage: 'Continue',
|
||||
}),
|
||||
});
|
||||
break;
|
||||
case '/overview/beats/enroll':
|
||||
actions.push({
|
||||
goTo: '/overview/beats/enroll',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.enrollAnotherBeatButtonLabel',
|
||||
defaultMessage: 'Enroll another Beat',
|
||||
}),
|
||||
newToken: true,
|
||||
});
|
||||
actions.push({
|
||||
goTo: '/overview/beats',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.doneButtonLabel',
|
||||
defaultMessage: 'Done',
|
||||
}),
|
||||
clearToken: true,
|
||||
});
|
||||
break;
|
||||
if (this.props.enrollmentToken && !this.state.enrolledBeat) {
|
||||
this.waitForTokenToEnrollBeat();
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -229,10 +198,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
/>
|
||||
{`//`}
|
||||
{window.location.host}
|
||||
{this.props.libs.framework.baseURLPath
|
||||
? this.props.libs.framework.baseURLPath
|
||||
: ''}{' '}
|
||||
{this.props.urlState.enrollmentToken}
|
||||
{this.props.frameworkBasePath} {this.props.enrollmentToken}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -309,38 +275,9 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
|
|||
/>
|
||||
<br />
|
||||
<br />
|
||||
{actions.map(action => (
|
||||
<EuiButton
|
||||
key={action.name}
|
||||
size="s"
|
||||
color="primary"
|
||||
style={{ marginLeft: 10 }}
|
||||
onClick={async () => {
|
||||
if (action.clearToken) {
|
||||
this.props.setUrlState({ enrollmentToken: '' });
|
||||
}
|
||||
|
||||
if (action.newToken) {
|
||||
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
|
||||
|
||||
this.props.setUrlState({ enrollmentToken });
|
||||
return this.setState({
|
||||
enrolledBeat: null,
|
||||
});
|
||||
}
|
||||
goTo(action.goTo);
|
||||
}}
|
||||
>
|
||||
{action.name}
|
||||
</EuiButton>
|
||||
))}
|
||||
</EuiModalBody>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const EnrollBeatPageUi = withUrlState<BeatsProps>(EnrollBeat);
|
||||
|
||||
export const EnrollBeatPage = injectI18n(EnrollBeatPageUi);
|
|
@ -4,11 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type FlatObject<T> = { [Key in keyof T]: string };
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface AppURLState {
|
||||
beatsKBar?: string;
|
||||
tagsKBar?: string;
|
||||
enrollmentToken?: string;
|
||||
createdTag?: string;
|
||||
}
|
||||
export const Background = styled.div`
|
||||
flex-grow: 1;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
`;
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBreadcrumbDefinition,
|
||||
EuiHeader,
|
||||
EuiHeaderBreadcrumbs,
|
||||
EuiHeaderSection,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface HeaderProps {
|
||||
breadcrumbs?: EuiBreadcrumbDefinition[];
|
||||
}
|
||||
|
||||
export class Header extends React.PureComponent<HeaderProps> {
|
||||
public render() {
|
||||
const { breadcrumbs = [] } = this.props;
|
||||
|
||||
return (
|
||||
<HeaderWrapper>
|
||||
<EuiHeaderSection>
|
||||
<EuiHeaderBreadcrumbs breadcrumbs={[...breadcrumbs]} />
|
||||
</EuiHeaderSection>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HeaderWrapper = styled(EuiHeader)`
|
||||
height: 29px;
|
||||
`;
|
|
@ -11,8 +11,6 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModal,
|
||||
EuiOverlayMask,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
|
@ -21,13 +19,11 @@ import {
|
|||
interface LayoutProps {
|
||||
title: string;
|
||||
actionSection?: React.ReactNode;
|
||||
modalRender?: () => React.ReactNode;
|
||||
modalClosePath?: string;
|
||||
}
|
||||
|
||||
export const NoDataLayout: React.SFC<LayoutProps> = withRouter<any>(
|
||||
({ actionSection, title, modalRender, modalClosePath, children, history }) => {
|
||||
const modalContent = modalRender && modalRender();
|
||||
({ actionSection, title, modalClosePath, children, history }) => {
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
|
@ -44,18 +40,6 @@ export const NoDataLayout: React.SFC<LayoutProps> = withRouter<any>(
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageBody>
|
||||
{modalContent && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
onClose={() => {
|
||||
history.push(modalClosePath);
|
||||
}}
|
||||
style={{ width: '640px' }}
|
||||
>
|
||||
{modalContent}
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiModal,
|
||||
EuiOverlayMask,
|
||||
EuiHeader,
|
||||
EuiHeaderBreadcrumbs,
|
||||
EuiHeaderSection,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
|
@ -18,45 +16,85 @@ import {
|
|||
EuiPageHeaderSection,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { BreadcrumbConsumer } from '../navigation/breadcrumb';
|
||||
|
||||
type RenderCallback = ((component: () => JSX.Element) => void);
|
||||
interface PrimaryLayoutProps {
|
||||
title: string;
|
||||
actionSection?: React.ReactNode;
|
||||
modalRender?: () => React.ReactNode;
|
||||
modalClosePath?: string;
|
||||
hideBreadcrumbs?: boolean;
|
||||
}
|
||||
export class PrimaryLayout extends Component<PrimaryLayoutProps> {
|
||||
private actionSection: (() => JSX.Element) | null = null;
|
||||
constructor(props: PrimaryLayoutProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
export const PrimaryLayout: React.SFC<PrimaryLayoutProps> = withRouter<any>(
|
||||
({ actionSection, title, modalRender, modalClosePath, children, history }) => {
|
||||
const modalContent = modalRender && modalRender();
|
||||
public render() {
|
||||
const children: (callback: RenderCallback) => void | ReactNode = this.props.children as any;
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>{title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection> {actionSection}</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>{children}</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
{modalContent && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
onClose={() => {
|
||||
history.push(modalClosePath);
|
||||
}}
|
||||
style={{ width: '640px' }}
|
||||
>
|
||||
{modalContent}
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
<React.Fragment>
|
||||
{!this.props.hideBreadcrumbs && (
|
||||
<BreadcrumbConsumer>
|
||||
{({ breadcrumbs }) => (
|
||||
<HeaderWrapper>
|
||||
<EuiHeaderSection>
|
||||
<EuiHeaderBreadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
href: '#/management',
|
||||
text: i18n.translate('xpack.beatsManagement.breadcrumb.managementTitle', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
href: '#/management/beats_management',
|
||||
text: i18n.translate('xpack.beatsManagement.breadcrumb.beatsTitle', {
|
||||
defaultMessage: 'Beats',
|
||||
}),
|
||||
},
|
||||
...breadcrumbs,
|
||||
]}
|
||||
/>
|
||||
</EuiHeaderSection>
|
||||
</HeaderWrapper>
|
||||
)}
|
||||
</BreadcrumbConsumer>
|
||||
)}
|
||||
</EuiPage>
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>{this.props.title}</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
{(this.actionSection && this.actionSection()) || this.props.actionSection}
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentBody>
|
||||
{(children && typeof children === 'function'
|
||||
? children(this.renderAction)
|
||||
: children) || <span />}
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
) as any;
|
||||
|
||||
private renderAction = (component: () => JSX.Element) => {
|
||||
this.actionSection = component;
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
const HeaderWrapper = styled(EuiHeader)`
|
||||
height: 29px;
|
||||
`;
|
||||
|
|
|
@ -22,7 +22,6 @@ interface LayoutProps {
|
|||
walkthroughSteps: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
activePath: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import * as React from 'react';
|
||||
|
||||
export const Loading: React.SFC<{}> = () => (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import { RouteProps } from 'react-router';
|
||||
import { BASE_PATH } from 'x-pack/plugins/beats_management/common/constants';
|
||||
import { BreadcrumbConsumer } from './consumer';
|
||||
import { Breadcrumb as BreadcrumbData, BreadcrumbContext } from './types';
|
||||
|
||||
interface BreadcrumbManagerProps extends RouteProps {
|
||||
text: string;
|
||||
href: string;
|
||||
parents?: BreadcrumbData[];
|
||||
context: BreadcrumbContext;
|
||||
}
|
||||
|
||||
class BreadcrumbManager extends Component<BreadcrumbManagerProps, {}, BreadcrumbContext> {
|
||||
public componentWillUnmount() {
|
||||
const { text, href, context } = this.props;
|
||||
|
||||
context.removeCrumb({
|
||||
text,
|
||||
href,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const { text, href, parents, context } = this.props;
|
||||
context.addCrumb(
|
||||
{
|
||||
text,
|
||||
href,
|
||||
},
|
||||
parents
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <span />;
|
||||
}
|
||||
}
|
||||
|
||||
interface BreadcrumbProps extends RouteProps {
|
||||
title: string;
|
||||
path: string;
|
||||
parentBreadcrumbs?: BreadcrumbData[];
|
||||
}
|
||||
|
||||
export const Breadcrumb: React.SFC<BreadcrumbProps> = ({ title, path, parentBreadcrumbs }) => (
|
||||
<BreadcrumbConsumer>
|
||||
{context => (
|
||||
<BreadcrumbManager
|
||||
text={title}
|
||||
href={`#${BASE_PATH}${path}`}
|
||||
parents={parentBreadcrumbs}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</BreadcrumbConsumer>
|
||||
);
|
|
@ -6,4 +6,4 @@
|
|||
|
||||
export { BreadcrumbProvider } from './provider';
|
||||
export { BreadcrumbConsumer } from './consumer';
|
||||
export { RouteWithBreadcrumb } from './route_with_breadcrumb';
|
||||
export { Breadcrumb } from './breadcrumb';
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { SFC } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
interface RouteConfig {
|
||||
path: string;
|
||||
component: React.ComponentType<any>;
|
||||
routes?: RouteConfig[];
|
||||
}
|
||||
|
||||
export const ChildRoutes: SFC<{
|
||||
routes?: RouteConfig[];
|
||||
useSwitch?: boolean;
|
||||
[other: string]: any;
|
||||
}> = ({ routes, useSwitch = true, ...rest }) => {
|
||||
if (!routes) {
|
||||
return null;
|
||||
}
|
||||
const Parent = useSwitch ? Switch : React.Fragment;
|
||||
return (
|
||||
<Parent>
|
||||
{routes.map(route => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
render={routeProps => {
|
||||
const Component = route.component;
|
||||
return <Component {...routeProps} routes={route.routes} {...rest} />;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Parent>
|
||||
);
|
||||
};
|
|
@ -13,6 +13,7 @@ export function ConnectedLinkComponent({
|
|||
path,
|
||||
query,
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
location: any;
|
||||
|
@ -30,7 +31,7 @@ export function ConnectedLinkComponent({
|
|||
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
children={children}
|
||||
to={{ ...location, ...props.to, pathname, query }}
|
||||
className={`euiLink euiLink--primary ${props.className || ''}`}
|
||||
/>
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { RouteProps } from 'react-router';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { BreadcrumbConsumer } from './consumer';
|
||||
import { Breadcrumb, BreadcrumbContext } from './types';
|
||||
|
||||
interface WrappedRouteWithBreadcrumbProps extends RouteProps {
|
||||
text: string;
|
||||
href: string;
|
||||
parents?: Breadcrumb[];
|
||||
context: BreadcrumbContext;
|
||||
}
|
||||
|
||||
class WrappedRouteWithBreadcrumb extends Component<
|
||||
WrappedRouteWithBreadcrumbProps,
|
||||
{},
|
||||
BreadcrumbContext
|
||||
> {
|
||||
public componentWillUnmount() {
|
||||
const { text, href, context } = this.props;
|
||||
|
||||
context.removeCrumb({
|
||||
text,
|
||||
href,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const { text, href, parents, context } = this.props;
|
||||
context.addCrumb(
|
||||
{
|
||||
text,
|
||||
href,
|
||||
},
|
||||
parents
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
type titleCallback = (
|
||||
urlParams: {
|
||||
[key: string]: string;
|
||||
}
|
||||
) => string;
|
||||
interface RouteWithBreadcrumbProps extends RouteProps {
|
||||
title: string | titleCallback;
|
||||
path: string;
|
||||
parentBreadcrumbs?: Breadcrumb[];
|
||||
}
|
||||
|
||||
export const RouteWithBreadcrumb: React.SFC<RouteWithBreadcrumbProps> = ({
|
||||
title,
|
||||
render,
|
||||
component: RouteComponent,
|
||||
parentBreadcrumbs,
|
||||
...props
|
||||
}) => (
|
||||
<Route
|
||||
{...props}
|
||||
render={renderProps => {
|
||||
return (
|
||||
<BreadcrumbConsumer>
|
||||
{context => (
|
||||
<WrappedRouteWithBreadcrumb
|
||||
parents={parentBreadcrumbs}
|
||||
href={props.path}
|
||||
text={typeof title === 'function' ? title(renderProps.match.params) : title}
|
||||
context={context}
|
||||
>
|
||||
{render && render(renderProps)}
|
||||
{RouteComponent && <RouteComponent {...renderProps} />}
|
||||
</WrappedRouteWithBreadcrumb>
|
||||
)}
|
||||
</BreadcrumbConsumer>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { AutocompleteField } from '../autocomplete_field/index';
|
||||
import { OptionControl } from '../table_controls';
|
||||
import { OptionControl } from './controls/index';
|
||||
import { AssignmentOptions as AssignmentOptionsType, KueryBarProps } from './table';
|
||||
|
||||
interface ControlBarProps {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { EuiIcon } from '@elastic/eui';
|
|||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { isArray } from 'lodash';
|
||||
import React from 'react';
|
||||
import { AssignmentControlSchema } from '../table';
|
||||
import { AssignmentControlSchema } from '../index';
|
||||
import { AssignmentActionType } from '../table';
|
||||
import { ActionControl } from './action_control';
|
||||
import { TagBadgeList } from './tag_badge_list';
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { TABLE_CONFIG } from '../../../common/constants';
|
||||
import { TagBadge } from '../tag/tag_badge';
|
||||
import { TABLE_CONFIG } from '../../../../common/constants';
|
||||
import { TagBadge } from '../../tag/tag_badge';
|
||||
|
||||
interface TagAssignmentProps {
|
||||
tag: any;
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { AssignmentActionType } from '../table/table';
|
||||
import { AssignmentActionType } from '../index';
|
||||
import { TagAssignment } from './tag_assignment';
|
||||
|
||||
interface TagBadgeListProps {
|
|
@ -10,7 +10,7 @@ import { first, sortBy, sortByOrder, uniq } from 'lodash';
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { ConnectedLink } from '../connected_link';
|
||||
import { ConnectedLink } from '../navigation/connected_link';
|
||||
import { TagBadge } from '../tag';
|
||||
|
||||
export interface ColumnDefinition {
|
||||
|
@ -61,7 +61,7 @@ export const BeatsTableType: TableType = {
|
|||
defaultMessage: 'Beat name',
|
||||
}),
|
||||
render: (name: string, beat: CMPopulatedBeat) => (
|
||||
<ConnectedLink path={`/beat/${beat.id}`}>{name}</ConnectedLink>
|
||||
<ConnectedLink path={`/beat/${beat.id}/details`}>{name}</ConnectedLink>
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
|
@ -91,7 +91,6 @@ export const BeatsTableType: TableType = {
|
|||
sortable: false,
|
||||
},
|
||||
{
|
||||
// TODO: update to use actual metadata field
|
||||
field: 'config_status',
|
||||
name: i18n.translate('xpack.beatsManagement.beatsTable.configStatusTitle', {
|
||||
defaultMessage: 'Config Status',
|
||||
|
|
|
@ -10,7 +10,7 @@ import yaml from 'js-yaml';
|
|||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { ConfigurationBlock } from '../../../../common/domain_types';
|
||||
import { YamlConfigSchema } from '../../../lib/lib';
|
||||
import { YamlConfigSchema } from '../../../lib/types';
|
||||
import {
|
||||
FormsyEuiCodeEditor,
|
||||
FormsyEuiFieldText,
|
||||
|
|
|
@ -17,11 +17,9 @@ import {
|
|||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
// @ts-ignore
|
||||
EuiHorizontalRule,
|
||||
// @ts-ignore
|
||||
EuiSearchBar,
|
||||
// @ts-ignore
|
||||
EuiSelect,
|
||||
// @ts-ignore
|
||||
EuiTabbedContent,
|
||||
|
|
|
@ -19,26 +19,23 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import 'brace/mode/yaml';
|
||||
import 'brace/theme/github';
|
||||
import { isEqual } from 'lodash';
|
||||
import React from 'react';
|
||||
import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { ConfigList } from '../config_list';
|
||||
import { AssignmentActionType, Table } from '../table';
|
||||
import { BeatsTableType } from '../table';
|
||||
import { tagConfigAssignmentOptions } from '../table';
|
||||
import { AssignmentActionType, BeatsTableType, Table, tagConfigAssignmentOptions } from '../table';
|
||||
import { ConfigView } from './config_view';
|
||||
import { TagBadge } from './tag_badge';
|
||||
|
||||
interface TagEditProps {
|
||||
mode: 'edit' | 'create';
|
||||
tag: Pick<BeatTag, Exclude<keyof BeatTag, 'last_updated'>>;
|
||||
onDetachBeat: (beatIds: string[]) => void;
|
||||
onDetachBeat?: (beatIds: string[]) => void;
|
||||
onTagChange: (field: keyof BeatTag, value: string) => any;
|
||||
attachedBeats: CMBeat[] | null;
|
||||
intl: InjectedIntl;
|
||||
attachedBeats?: CMBeat[];
|
||||
}
|
||||
|
||||
interface TagEditState {
|
||||
|
@ -47,7 +44,7 @@ interface TagEditState {
|
|||
selectedConfigIndex?: number;
|
||||
}
|
||||
|
||||
class TagEditUi extends React.PureComponent<TagEditProps, TagEditState> {
|
||||
export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
|
||||
constructor(props: TagEditProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -58,7 +55,7 @@ class TagEditUi extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { tag, attachedBeats, intl } = this.props;
|
||||
const { tag, attachedBeats } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup>
|
||||
|
@ -99,18 +96,16 @@ class TagEditUi extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
name="name"
|
||||
isInvalid={!!this.getNameError(tag.id)}
|
||||
onChange={this.updateTag('id')}
|
||||
disabled={this.props.mode === 'edit'}
|
||||
disabled={!!this.props.onDetachBeat}
|
||||
value={tag.id}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tag.tagNamePlaceholder',
|
||||
placeholder={i18n.translate('xpack.beatsManagement.tag.tagNamePlaceholder', {
|
||||
defaultMessage: 'Tag name (required)',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{this.props.mode === 'create' && (
|
||||
{!this.props.onDetachBeat && (
|
||||
<EuiFormRow
|
||||
label={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tag.tagColorLabel',
|
||||
label={i18n.translate('xpack.beatsManagement.tag.tagColorLabel', {
|
||||
defaultMessage: 'Tag Color',
|
||||
})}
|
||||
>
|
||||
|
@ -236,10 +231,8 @@ class TagEditUi extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
}
|
||||
|
||||
private getNameError = (name: string) => {
|
||||
const { intl } = this.props;
|
||||
if (name && name !== '' && name.search(/^[a-zA-Z0-9-]+$/) === -1) {
|
||||
return intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tag.tagName.validationErrorMessage',
|
||||
return i18n.translate('xpack.beatsManagement.tag.tagName.validationErrorMessage', {
|
||||
defaultMessage: 'Tag name must consist of letters, numbers, and dashes only',
|
||||
});
|
||||
} else {
|
||||
|
@ -251,15 +244,14 @@ class TagEditUi extends React.PureComponent<TagEditProps, TagEditState> {
|
|||
switch (action) {
|
||||
case AssignmentActionType.Delete:
|
||||
const { selection } = this.state.tableRef.current.state;
|
||||
this.props.onDetachBeat(selection.map((beat: any) => beat.id));
|
||||
if (this.props.onDetachBeat) {
|
||||
this.props.onDetachBeat(selection.map((beat: any) => beat.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO this should disable save button on bad validations
|
||||
private updateTag = (key: keyof BeatTag, value?: any) =>
|
||||
value !== undefined
|
||||
? this.props.onTagChange(key, value)
|
||||
: (e: any) => this.props.onTagChange(key, e.target ? e.target.value : e);
|
||||
}
|
||||
|
||||
export const TagEdit = injectI18n(TagEditUi);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { YamlConfigSchema } from './lib/lib';
|
||||
import { YamlConfigSchema } from './lib/types';
|
||||
|
||||
const filebeatInputConfig: YamlConfigSchema[] = [
|
||||
{
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { supportedConfigs } from './config_schemas';
|
||||
import { YamlConfigSchema } from './lib/lib';
|
||||
import { YamlConfigSchema } from './lib/types';
|
||||
|
||||
interface ConfigSchema {
|
||||
text: string;
|
||||
|
@ -226,10 +226,9 @@ export const getSupportedConfig = () => {
|
|||
}
|
||||
|
||||
translatedConfigs = cloneDeep(supportedConfigs);
|
||||
|
||||
translatedConfigs.forEach(({ text, config }) => {
|
||||
translatedConfigs.forEach(({ text, config }, index) => {
|
||||
if (text) {
|
||||
text = supportedConfigLabelsMap.get(text) || '';
|
||||
translatedConfigs[index].text = supportedConfigLabelsMap.get(text) || '';
|
||||
}
|
||||
|
||||
config.forEach(yanlConfig => {
|
||||
|
|
98
x-pack/plugins/beats_management/public/containers/beats.ts
Normal file
98
x-pack/plugins/beats_management/public/containers/beats.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Container } from 'unstated';
|
||||
import { CMPopulatedBeat } from './../../common/domain_types';
|
||||
import { BeatsTagAssignment } from './../../server/lib/adapters/beats/adapter_types';
|
||||
import { FrontendLibs } from './../lib/types';
|
||||
|
||||
interface ContainerState {
|
||||
list: CMPopulatedBeat[];
|
||||
}
|
||||
|
||||
export class BeatsContainer extends Container<ContainerState> {
|
||||
private query?: string;
|
||||
constructor(private readonly libs: FrontendLibs) {
|
||||
super();
|
||||
this.state = {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
|
||||
public getBeatWithToken = async (token: string) => {
|
||||
const beat = await this.libs.beats.getBeatWithToken(token);
|
||||
|
||||
if (beat) {
|
||||
this.setState({
|
||||
list: [beat as CMPopulatedBeat, ...this.state.list],
|
||||
});
|
||||
return beat as CMPopulatedBeat;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
public reload = async (kuery?: string) => {
|
||||
if (kuery) {
|
||||
this.query = await this.libs.elasticsearch.convertKueryToEsQuery(kuery);
|
||||
} else {
|
||||
this.query = undefined;
|
||||
}
|
||||
const beats = await this.libs.beats.getAll(this.query);
|
||||
|
||||
this.setState({
|
||||
list: beats,
|
||||
});
|
||||
};
|
||||
|
||||
public deactivate = async (beats: CMPopulatedBeat[]) => {
|
||||
for (const beat of beats) {
|
||||
await this.libs.beats.update(beat.id, { active: false });
|
||||
}
|
||||
|
||||
// because the compile code above has a very minor race condition, we wait,
|
||||
// the max race condition time is really 10ms but doing 100 to be safe
|
||||
setTimeout(async () => {
|
||||
await this.reload(this.query);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
public toggleTagAssignment = async (tagId: string, beats: CMPopulatedBeat[]) => {
|
||||
if (beats.some(beat => beat.full_tags.some(({ id }) => id === tagId))) {
|
||||
await this.removeTagsFromBeats(beats, tagId);
|
||||
return 'removed';
|
||||
}
|
||||
await this.assignTagsToBeats(beats, tagId);
|
||||
return 'added';
|
||||
};
|
||||
|
||||
public removeTagsFromBeats = async (beats: CMPopulatedBeat[] | string[], tagId: string) => {
|
||||
if (!beats.length) {
|
||||
return false;
|
||||
}
|
||||
const assignments = createBeatTagAssignments(beats, tagId);
|
||||
await this.libs.beats.removeTagsFromBeats(assignments);
|
||||
await this.reload(this.query);
|
||||
};
|
||||
|
||||
public assignTagsToBeats = async (beats: CMPopulatedBeat[] | string[], tagId: string) => {
|
||||
if (!beats.length) {
|
||||
return false;
|
||||
}
|
||||
const assignments = createBeatTagAssignments(beats, tagId);
|
||||
await this.libs.beats.assignTagsToBeats(assignments);
|
||||
await this.reload(this.query);
|
||||
};
|
||||
}
|
||||
|
||||
function createBeatTagAssignments(
|
||||
beats: CMPopulatedBeat[] | string[],
|
||||
tagId: string
|
||||
): BeatsTagAssignment[] {
|
||||
if (typeof beats[0] === 'string') {
|
||||
return (beats as string[]).map(id => ({ beatId: id, tag: tagId }));
|
||||
} else {
|
||||
return (beats as CMPopulatedBeat[]).map(({ id }) => ({ beatId: id, tag: tagId }));
|
||||
}
|
||||
}
|
45
x-pack/plugins/beats_management/public/containers/tags.ts
Normal file
45
x-pack/plugins/beats_management/public/containers/tags.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Container } from 'unstated';
|
||||
import { BeatTag } from '../../common/domain_types';
|
||||
import { FrontendLibs } from '../lib/types';
|
||||
|
||||
interface ContainerState {
|
||||
list: BeatTag[];
|
||||
}
|
||||
|
||||
export class TagsContainer extends Container<ContainerState> {
|
||||
private query?: string;
|
||||
constructor(private readonly libs: FrontendLibs) {
|
||||
super();
|
||||
this.state = {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
public reload = async (kuery?: string) => {
|
||||
if (kuery) {
|
||||
this.query = await this.libs.elasticsearch.convertKueryToEsQuery(kuery);
|
||||
} else {
|
||||
this.query = undefined;
|
||||
}
|
||||
|
||||
const tags = await this.libs.tags.getAll(this.query);
|
||||
|
||||
this.setState({
|
||||
list: tags,
|
||||
});
|
||||
};
|
||||
|
||||
public delete = async (tags: BeatTag[]) => {
|
||||
const tagIds = tags.map((tag: BeatTag) => tag.id);
|
||||
const success = await this.libs.tags.delete(tagIds);
|
||||
if (success) {
|
||||
this.reload(this.query);
|
||||
}
|
||||
return success;
|
||||
};
|
||||
}
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
|
||||
import { FrontendLibs } from '../lib/lib';
|
||||
import { FrontendLibs } from '../lib/types';
|
||||
import { RendererFunction } from '../utils/typed_react';
|
||||
|
||||
interface WithKueryAutocompletionLifecycleProps {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { parse, stringify } from 'querystring';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { FlatObject } from '../app';
|
||||
import { FlatObject } from '../frontend_types';
|
||||
import { RendererFunction } from '../utils/typed_react';
|
||||
|
||||
type StateCallback<T> = (previousState: T) => T;
|
||||
|
@ -88,7 +88,9 @@ export class WithURLStateComponent<URLState extends object> extends React.Compon
|
|||
}
|
||||
export const WithURLState = withRouter<any>(WithURLStateComponent);
|
||||
|
||||
export function withUrlState<OP>(UnwrappedComponent: React.ComponentType<OP>): React.SFC<any> {
|
||||
export function withUrlState<OP>(
|
||||
UnwrappedComponent: React.ComponentType<OP & URLStateProps>
|
||||
): React.SFC<any> {
|
||||
return (origProps: OP) => {
|
||||
return (
|
||||
<WithURLState>
|
||||
|
|
34
x-pack/plugins/beats_management/public/frontend_types.d.ts
vendored
Normal file
34
x-pack/plugins/beats_management/public/frontend_types.d.ts
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { BeatsContainer } from './containers/beats';
|
||||
import { TagsContainer } from './containers/tags';
|
||||
import { URLStateProps } from './containers/with_url_state';
|
||||
import { FrontendLibs } from './lib/types';
|
||||
|
||||
export type FlatObject<T> = { [Key in keyof T]: string };
|
||||
|
||||
export interface AppURLState {
|
||||
beatsKBar?: string;
|
||||
tagsKBar?: string;
|
||||
enrollmentToken?: string;
|
||||
createdTag?: string;
|
||||
}
|
||||
|
||||
export interface RouteConfig {
|
||||
path: string;
|
||||
component: React.ComponentType<any>;
|
||||
routes?: RouteConfig[];
|
||||
}
|
||||
|
||||
export interface AppPageProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
|
||||
libs: FrontendLibs;
|
||||
containers: {
|
||||
beats: BeatsContainer;
|
||||
tags: TagsContainer;
|
||||
};
|
||||
routes?: RouteConfig[];
|
||||
}
|
|
@ -8,30 +8,60 @@ import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { Provider as UnstatedProvider, Subscribe } from 'unstated';
|
||||
import { BASE_PATH } from '../common/constants';
|
||||
import { BreadcrumbProvider } from './components/route_with_breadcrumb';
|
||||
import { Background } from './components/layouts/background';
|
||||
import { BreadcrumbProvider } from './components/navigation/breadcrumb';
|
||||
import { BeatsContainer } from './containers/beats';
|
||||
import { TagsContainer } from './containers/tags';
|
||||
import { compose } from './lib/compose/kibana';
|
||||
import { FrontendLibs } from './lib/lib';
|
||||
import { PageRouter } from './router';
|
||||
import { FrontendLibs } from './lib/types';
|
||||
import { AppRouter } from './router';
|
||||
|
||||
function startApp(libs: FrontendLibs) {
|
||||
libs.framework.registerManagementSection(
|
||||
'beats',
|
||||
i18n.translate('xpack.beatsManagement.managementMainPage.centralManagementLinkLabel', {
|
||||
defaultMessage: 'Central Management (Beta)',
|
||||
}),
|
||||
BASE_PATH
|
||||
);
|
||||
libs.framework.render(
|
||||
<I18nProvider>
|
||||
<ThemeProvider theme={{ eui: euiVars }}>
|
||||
<BreadcrumbProvider>
|
||||
<PageRouter libs={libs} />
|
||||
</BreadcrumbProvider>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
async function startApp(libs: FrontendLibs) {
|
||||
libs.framework.renderUIAtPath(
|
||||
BASE_PATH,
|
||||
<ThemeProvider theme={{ eui: euiVars }}>
|
||||
<I18nProvider>
|
||||
<HashRouter basename="/management/beats_management">
|
||||
<UnstatedProvider inject={[new BeatsContainer(libs), new TagsContainer(libs)]}>
|
||||
<BreadcrumbProvider>
|
||||
<Subscribe to={[BeatsContainer, TagsContainer]}>
|
||||
{(beats: BeatsContainer, tags: TagsContainer) => (
|
||||
<Background>
|
||||
<AppRouter libs={libs} beatsContainer={beats} tagsContainer={tags} />
|
||||
</Background>
|
||||
)}
|
||||
</Subscribe>
|
||||
</BreadcrumbProvider>
|
||||
</UnstatedProvider>
|
||||
</HashRouter>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
libs.framework.getUISetting('k7design') ? 'management' : 'self'
|
||||
);
|
||||
|
||||
await libs.framework.waitUntilFrameworkReady();
|
||||
|
||||
if (libs.framework.licenseIsAtLeast('standard')) {
|
||||
libs.framework.registerManagementSection({
|
||||
id: 'beats',
|
||||
name: i18n.translate('xpack.beatsManagement.centralManagementSectionLabel', {
|
||||
defaultMessage: 'Beats',
|
||||
}),
|
||||
iconName: 'logoBeats',
|
||||
});
|
||||
|
||||
libs.framework.registerManagementUI({
|
||||
sectionId: 'beats',
|
||||
name: i18n.translate('xpack.beatsManagement.centralManagementLinkLabel', {
|
||||
defaultMessage: 'Central Management (Beta)',
|
||||
}),
|
||||
basePath: BASE_PATH,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startApp(compose());
|
||||
|
|
|
@ -24,7 +24,7 @@ export class RestBeatsAdapter implements CMBeatsAdapter {
|
|||
return beat;
|
||||
}
|
||||
|
||||
public async getAll(ESQuery?: any): Promise<CMBeat[]> {
|
||||
public async getAll(ESQuery?: string): Promise<CMBeat[]> {
|
||||
return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents/all', { ESQuery })).beats;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { AutocompleteSuggestion, getAutocompleteProvider } from 'ui/autocomplete_providers';
|
||||
// @ts-ignore TODO type this
|
||||
import { fromKueryExpression, toElasticsearchQuery } from 'ui/kuery';
|
||||
import { RestAPIAdapter } from '../rest_api/adapter_types';
|
||||
import { ElasticsearchAdapter } from './adapter_types';
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { LICENSES } from './../../../../common/constants/security';
|
||||
|
||||
export interface FrameworkAdapter {
|
||||
// Instance vars
|
||||
info: FrameworkInfo;
|
||||
currentUser: FrameworkUser;
|
||||
// Methods
|
||||
waitUntilFrameworkReady(): Promise<void>;
|
||||
renderUIAtPath(path: string, component: React.ReactElement<any>): void;
|
||||
registerManagementSection(settings: {
|
||||
id?: string;
|
||||
name: string;
|
||||
iconName: string;
|
||||
order?: number;
|
||||
}): void;
|
||||
registerManagementUI(settings: {
|
||||
id?: string;
|
||||
name: string;
|
||||
basePath: string;
|
||||
visable?: boolean;
|
||||
order?: number;
|
||||
}): void;
|
||||
setUISettings(key: string, value: any): void;
|
||||
getUISetting(key: 'k7design'): boolean;
|
||||
}
|
||||
|
||||
export const RuntimeFrameworkInfo = t.type({
|
||||
basePath: t.string,
|
||||
k7Design: t.boolean,
|
||||
license: t.type({
|
||||
type: t.union(LICENSES.map(s => t.literal(s))),
|
||||
expired: t.boolean,
|
||||
expiry_date_in_millis: t.number,
|
||||
}),
|
||||
security: t.type({
|
||||
enabled: t.boolean,
|
||||
available: t.boolean,
|
||||
}),
|
||||
settings: t.type({
|
||||
encryptionKey: t.string,
|
||||
enrollmentTokensTtlInSeconds: t.number,
|
||||
defaultUserRoles: t.array(t.string),
|
||||
}),
|
||||
});
|
||||
|
||||
export interface FrameworkInfo extends t.TypeOf<typeof RuntimeFrameworkInfo> {}
|
||||
|
||||
interface ManagementSection {
|
||||
register(
|
||||
sectionId: string,
|
||||
options: {
|
||||
visible: boolean;
|
||||
display: string;
|
||||
order: number;
|
||||
url: string;
|
||||
}
|
||||
): void;
|
||||
}
|
||||
export interface ManagementAPI {
|
||||
getSection(sectionId: string): ManagementSection;
|
||||
hasItem(sectionId: string): boolean;
|
||||
register(sectionId: string, options: { display: string; icon: string; order: number }): void;
|
||||
}
|
||||
|
||||
export const RuntimeFrameworkUser = t.interface(
|
||||
{
|
||||
username: t.string,
|
||||
roles: t.array(t.string),
|
||||
full_name: t.union([t.null, t.string]),
|
||||
email: t.union([t.null, t.string]),
|
||||
enabled: t.boolean,
|
||||
},
|
||||
'FrameworkUser'
|
||||
);
|
||||
export interface FrameworkUser extends t.TypeOf<typeof RuntimeFrameworkUser> {}
|
|
@ -4,49 +4,64 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IModule, IScope } from 'angular';
|
||||
import { IScope } from 'angular';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UIRoutes } from 'ui/routes';
|
||||
import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types';
|
||||
import {
|
||||
BufferedKibanaServiceCall,
|
||||
FrameworkAdapter,
|
||||
KibanaAdapterServiceRefs,
|
||||
KibanaUIConfig,
|
||||
} from '../../lib';
|
||||
FrameworkInfo,
|
||||
FrameworkUser,
|
||||
ManagementAPI,
|
||||
RuntimeFrameworkInfo,
|
||||
RuntimeFrameworkUser,
|
||||
} from './adapter_types';
|
||||
interface IInjector {
|
||||
get(injectable: string): any;
|
||||
}
|
||||
|
||||
export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
||||
public appState: object;
|
||||
|
||||
private management: any;
|
||||
private adapterService: KibanaAdapterServiceProvider;
|
||||
private rootComponent: React.ReactElement<any> | null = null;
|
||||
private uiModule: IModule;
|
||||
private routes: any;
|
||||
private XPackInfoProvider: any;
|
||||
private xpackInfo: null | any;
|
||||
private chrome: any;
|
||||
private shieldUser: any;
|
||||
|
||||
constructor(
|
||||
uiModule: IModule,
|
||||
management: any,
|
||||
routes: any,
|
||||
chrome: any,
|
||||
XPackInfoProvider: any
|
||||
) {
|
||||
this.adapterService = new KibanaAdapterServiceProvider();
|
||||
this.management = management;
|
||||
this.uiModule = uiModule;
|
||||
this.routes = routes;
|
||||
this.chrome = chrome;
|
||||
this.XPackInfoProvider = XPackInfoProvider;
|
||||
this.appState = {};
|
||||
public get info() {
|
||||
if (this.xpackInfo) {
|
||||
return this.xpackInfo;
|
||||
} else {
|
||||
throw new Error('framework adapter must have init called before anything else');
|
||||
}
|
||||
}
|
||||
|
||||
public get baseURLPath(): string {
|
||||
return this.chrome.getBasePath();
|
||||
public get currentUser() {
|
||||
return this.shieldUser!;
|
||||
}
|
||||
private xpackInfo: FrameworkInfo | null = null;
|
||||
private adapterService: KibanaAdapterServiceProvider;
|
||||
private shieldUser: FrameworkUser | null = null;
|
||||
private settingSubscription: any;
|
||||
constructor(
|
||||
private readonly PLUGIN_ID: string,
|
||||
private readonly management: ManagementAPI,
|
||||
private readonly routes: UIRoutes,
|
||||
private readonly getBasePath: () => string,
|
||||
private readonly onKibanaReady: () => Promise<IInjector>,
|
||||
private readonly XPackInfoProvider: unknown,
|
||||
private readonly uiSettings: any
|
||||
) {
|
||||
this.adapterService = new KibanaAdapterServiceProvider();
|
||||
|
||||
this.settingSubscription = uiSettings.getUpdate$().subscribe({
|
||||
next: ({ key, newValue }: { key: string; newValue: boolean }) => {
|
||||
if (key === 'k7design' && this.xpackInfo) {
|
||||
this.xpackInfo.k7Design = newValue;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// We dont really want to have this, but it's needed to conditionaly render for k7 due to
|
||||
// when that data is needed.
|
||||
public getUISetting(key: 'k7design'): boolean {
|
||||
return this.uiSettings.get(key);
|
||||
}
|
||||
|
||||
public setUISettings = (key: string, value: any) => {
|
||||
|
@ -55,71 +70,137 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
|||
});
|
||||
};
|
||||
|
||||
public render = (component: React.ReactElement<any>) => {
|
||||
this.rootComponent = component;
|
||||
};
|
||||
public async waitUntilFrameworkReady(): Promise<void> {
|
||||
const $injector = await this.onKibanaReady();
|
||||
const Private: any = $injector.get('Private');
|
||||
|
||||
public hasValidLicense() {
|
||||
if (!this.xpackInfo) {
|
||||
return false;
|
||||
}
|
||||
return this.xpackInfo.get('features.beats_management.licenseValid', false);
|
||||
}
|
||||
|
||||
public licenseExpired() {
|
||||
if (!this.xpackInfo) {
|
||||
return false;
|
||||
}
|
||||
return this.xpackInfo.get('features.beats_management.licenseExpired', false);
|
||||
}
|
||||
|
||||
public securityEnabled() {
|
||||
if (!this.xpackInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.xpackInfo.get('features.beats_management.securityEnabled', false);
|
||||
}
|
||||
|
||||
public getDefaultUserRoles() {
|
||||
if (!this.xpackInfo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.xpackInfo.get('features.beats_management.defaultUserRoles');
|
||||
}
|
||||
|
||||
public getCurrentUser() {
|
||||
let xpackInfo: any;
|
||||
try {
|
||||
return this.shieldUser;
|
||||
xpackInfo = Private(this.XPackInfoProvider);
|
||||
} catch (e) {
|
||||
return null;
|
||||
xpackInfo = false;
|
||||
}
|
||||
|
||||
let xpackInfoUnpacked: FrameworkInfo;
|
||||
try {
|
||||
xpackInfoUnpacked = {
|
||||
basePath: this.getBasePath(),
|
||||
k7Design: this.uiSettings.get('k7design'),
|
||||
license: {
|
||||
type: xpackInfo ? xpackInfo.getLicense().type : 'oss',
|
||||
expired: xpackInfo ? !xpackInfo.getLicense().isActive : false,
|
||||
expiry_date_in_millis: xpackInfo ? xpackInfo.getLicense().expiryDateInMillis : 0,
|
||||
},
|
||||
security: {
|
||||
enabled: xpackInfo
|
||||
? xpackInfo.get(`features.${this.PLUGIN_ID}.security.enabled`, false)
|
||||
: false,
|
||||
available: xpackInfo
|
||||
? xpackInfo.get(`features.${this.PLUGIN_ID}.security.available`, false)
|
||||
: false,
|
||||
},
|
||||
settings: xpackInfo ? xpackInfo.get(`features.${this.PLUGIN_ID}.settings`) : {},
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(`Unexpected data structure from XPackInfoProvider, ${JSON.stringify(e)}`);
|
||||
}
|
||||
|
||||
const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked);
|
||||
if (assertData.isLeft()) {
|
||||
throw new Error(
|
||||
`Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}`
|
||||
);
|
||||
}
|
||||
this.xpackInfo = xpackInfoUnpacked;
|
||||
|
||||
try {
|
||||
this.shieldUser = await $injector.get('ShieldUser').getCurrent().$promise;
|
||||
const assertUser = RuntimeFrameworkUser.decode(this.shieldUser);
|
||||
|
||||
if (assertUser.isLeft()) {
|
||||
throw new Error(
|
||||
`Error parsing user info in ${this.PLUGIN_ID}, ${PathReporter.report(assertUser)[0]}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.shieldUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
public registerManagementSection(pluginId: string, displayName: string, basePath: string) {
|
||||
this.register(this.uiModule);
|
||||
|
||||
this.hookAngular(() => {
|
||||
if (this.hasValidLicense()) {
|
||||
const registerSection = () =>
|
||||
this.management.register(pluginId, {
|
||||
display: i18n.translate('xpack.beatsManagement.beatsDislayName', {
|
||||
defaultMessage: 'Beats',
|
||||
}), // TODO these need to be config options not hard coded in the adapter
|
||||
icon: 'logoBeats',
|
||||
order: 30,
|
||||
});
|
||||
const getSection = () => this.management.getSection(pluginId);
|
||||
const section = this.management.hasItem(pluginId) ? getSection() : registerSection();
|
||||
|
||||
section.register(pluginId, {
|
||||
visible: true,
|
||||
display: displayName,
|
||||
order: 30,
|
||||
url: `#${basePath}`,
|
||||
});
|
||||
public renderUIAtPath(
|
||||
path: string,
|
||||
component: React.ReactElement<any>,
|
||||
toController: 'management' | 'self' = 'self'
|
||||
) {
|
||||
const DOM_ELEMENT_NAME = this.PLUGIN_ID.replace('_', '-');
|
||||
const adapter = this;
|
||||
this.routes.when(
|
||||
`${path}${[...Array(6)].map((e, n) => `/:arg${n}?`).join('')}`, // Hack because angular 1 does not support wildcards
|
||||
{
|
||||
template:
|
||||
toController === 'self'
|
||||
? `<${DOM_ELEMENT_NAME}><div id="${DOM_ELEMENT_NAME}ReactRoot"></div></${DOM_ELEMENT_NAME}>`
|
||||
: `<kbn-management-app section="${this.PLUGIN_ID.replace('_', '-')}">
|
||||
<div id="${DOM_ELEMENT_NAME}ReactRoot" />
|
||||
</kbn-management-app>`,
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
controller: ($scope: any, $route: any) => {
|
||||
try {
|
||||
$scope.$$postDigest(() => {
|
||||
const elem = document.getElementById(`${DOM_ELEMENT_NAME}ReactRoot`);
|
||||
ReactDOM.render(component, elem);
|
||||
adapter.manageAngularLifecycle($scope, $route, elem);
|
||||
});
|
||||
$scope.$onInit = () => {
|
||||
$scope.topNavMenu = [];
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(`Error rendering Beats CM to the dom, ${e.message}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public registerManagementSection(settings: {
|
||||
id?: string;
|
||||
name: string;
|
||||
iconName: string;
|
||||
order?: number;
|
||||
}) {
|
||||
const sectionId = settings.id || this.PLUGIN_ID;
|
||||
|
||||
if (!this.management.hasItem(sectionId)) {
|
||||
this.management.register(sectionId, {
|
||||
display: settings.name,
|
||||
icon: settings.iconName,
|
||||
order: settings.order || 30,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public registerManagementUI(settings: {
|
||||
sectionId?: string;
|
||||
name: string;
|
||||
basePath: string;
|
||||
visable?: boolean;
|
||||
order?: number;
|
||||
}) {
|
||||
const sectionId = settings.sectionId || this.PLUGIN_ID;
|
||||
|
||||
if (!this.management.hasItem(sectionId)) {
|
||||
throw new Error(
|
||||
`registerManagementUI was called with a sectionId of ${sectionId}, and that is is not yet regestered as a section`
|
||||
);
|
||||
}
|
||||
|
||||
const section = this.management.getSection(sectionId);
|
||||
|
||||
section.register(sectionId, {
|
||||
visible: settings.visable || true,
|
||||
display: settings.name,
|
||||
order: settings.order || 30,
|
||||
url: `#${settings.basePath}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,58 +212,27 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
|
|||
if (lastRoute.$$route.template === currentRoute.$$route.template) {
|
||||
// this prevents angular from destroying scope
|
||||
$route.current = lastRoute;
|
||||
} else {
|
||||
if (elem) {
|
||||
ReactDOM.unmountComponentAtNode(elem);
|
||||
elem.remove();
|
||||
this.settingSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
});
|
||||
$scope.$on('$destroy', () => {
|
||||
if (deregister) {
|
||||
deregister();
|
||||
}
|
||||
|
||||
// manually unmount component when scope is destroyed
|
||||
if (elem) {
|
||||
ReactDOM.unmountComponentAtNode(elem);
|
||||
elem.remove();
|
||||
this.settingSubscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private hookAngular(done: () => any) {
|
||||
this.chrome.dangerouslyGetActiveInjector().then(async ($injector: any) => {
|
||||
const Private = $injector.get('Private');
|
||||
const xpackInfo = Private(this.XPackInfoProvider);
|
||||
|
||||
this.xpackInfo = xpackInfo;
|
||||
if (this.securityEnabled()) {
|
||||
try {
|
||||
this.shieldUser = await $injector.get('ShieldUser').getCurrent().$promise;
|
||||
} catch (e) {
|
||||
// errors when security disabled, even though we check first because angular
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
private register = (adapterModule: IModule) => {
|
||||
const adapter = this;
|
||||
this.routes.when(`/management/beats_management/:view?/:id?/:other?/:other2?`, {
|
||||
template:
|
||||
'<beats-cm><div id="beatsReactRoot" style="flex-grow: 1; height: 100vh; background: #f5f5f5"></div></beats-cm>',
|
||||
controllerAs: 'beatsManagement',
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
controller: class BeatsManagementController {
|
||||
constructor($scope: any, $route: any) {
|
||||
$scope.$$postDigest(() => {
|
||||
const elem = document.getElementById('beatsReactRoot');
|
||||
ReactDOM.render(adapter.rootComponent as React.ReactElement<any>, elem);
|
||||
adapter.manageAngularLifecycle($scope, $route, elem);
|
||||
});
|
||||
$scope.$onInit = () => {
|
||||
$scope.topNavMenu = [];
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { FlatObject } from '../../../app';
|
||||
import { FlatObject } from '../../../frontend_types';
|
||||
|
||||
export interface RestAPIAdapter {
|
||||
get<ResponseData>(url: string, query?: FlatObject<object>): Promise<ResponseData>;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { FlatObject } from '../../../app';
|
||||
import { FlatObject } from '../../../frontend_types';
|
||||
import { RestAPIAdapter } from './adapter_types';
|
||||
let globalAPI: AxiosInstance;
|
||||
|
||||
|
|
|
@ -8,6 +8,6 @@ import { BeatTag } from '../../../../common/domain_types';
|
|||
export interface CMTagsAdapter {
|
||||
getTagsWithIds(tagIds: string[]): Promise<BeatTag[]>;
|
||||
delete(tagIds: string[]): Promise<boolean>;
|
||||
getAll(): Promise<BeatTag[]>;
|
||||
getAll(ESQuery?: string): Promise<BeatTag[]>;
|
||||
upsertTag(tag: BeatTag): Promise<BeatTag | null>;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export class MemoryTagsAdapter implements CMTagsAdapter {
|
|||
return true;
|
||||
}
|
||||
|
||||
public async getAll() {
|
||||
public async getAll(ESQuery?: string) {
|
||||
return this.tagsDB;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ export class RestTagsAdapter implements CMTagsAdapter {
|
|||
return tags;
|
||||
}
|
||||
|
||||
public async getAll(): Promise<BeatTag[]> {
|
||||
return await this.REST.get<BeatTag[]>(`/api/beats/tags`);
|
||||
public async getAll(ESQuery: string): Promise<BeatTag[]> {
|
||||
return await this.REST.get<BeatTag[]>(`/api/beats/tags`, { ESQuery });
|
||||
}
|
||||
|
||||
public async delete(tagIds: string[]): Promise<boolean> {
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
CMAssignmentReturn,
|
||||
CMBeatsAdapter,
|
||||
} from './adapters/beats/adapter_types';
|
||||
import { FrontendDomainLibs } from './lib';
|
||||
import { FrontendDomainLibs } from './types';
|
||||
|
||||
export class BeatsLib {
|
||||
constructor(
|
||||
|
@ -20,44 +20,58 @@ export class BeatsLib {
|
|||
private readonly libs: { tags: FrontendDomainLibs['tags'] }
|
||||
) {}
|
||||
|
||||
/** Get a single beat using it's ID for lookup */
|
||||
public async get(id: string): Promise<CMPopulatedBeat | null> {
|
||||
const beat = await this.adapter.get(id);
|
||||
return beat ? (await this.mergeInTags([beat]))[0] : null;
|
||||
}
|
||||
|
||||
public async getBeatWithToken(enrollmentToken: string): Promise<CMBeat | null> {
|
||||
/** Get a single beat using the token it was enrolled in for lookup */
|
||||
public getBeatWithToken = async (enrollmentToken: string): Promise<CMBeat | null> => {
|
||||
const beat = await this.adapter.getBeatWithToken(enrollmentToken);
|
||||
return beat;
|
||||
}
|
||||
};
|
||||
|
||||
public async getBeatsWithTag(tagId: string): Promise<CMPopulatedBeat[]> {
|
||||
/** Get an array of beats that have a given tag id assigned to it */
|
||||
public getBeatsWithTag = async (tagId: string): Promise<CMPopulatedBeat[]> => {
|
||||
const beats = await this.adapter.getBeatsWithTag(tagId);
|
||||
return await this.mergeInTags(beats);
|
||||
}
|
||||
};
|
||||
|
||||
public async getAll(ESQuery?: any): Promise<CMPopulatedBeat[]> {
|
||||
// FIXME: This needs to be paginated https://github.com/elastic/kibana/issues/26022
|
||||
/** Get an array of all enrolled beats. */
|
||||
public getAll = async (ESQuery?: string): Promise<CMPopulatedBeat[]> => {
|
||||
const beats = await this.adapter.getAll(ESQuery);
|
||||
return await this.mergeInTags(beats);
|
||||
}
|
||||
};
|
||||
|
||||
public async update(id: string, beatData: Partial<CMBeat>): Promise<boolean> {
|
||||
/** Update a given beat via it's ID */
|
||||
public update = async (id: string, beatData: Partial<CMBeat>): Promise<boolean> => {
|
||||
return await this.adapter.update(id, beatData);
|
||||
}
|
||||
};
|
||||
|
||||
public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise<BeatsRemovalReturn[]> {
|
||||
/** unassign tags from beats using an array of tags and beats */
|
||||
public removeTagsFromBeats = async (
|
||||
removals: BeatsTagAssignment[]
|
||||
): Promise<BeatsRemovalReturn[]> => {
|
||||
return await this.adapter.removeTagsFromBeats(removals);
|
||||
}
|
||||
};
|
||||
|
||||
public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise<CMAssignmentReturn[]> {
|
||||
/** assign tags from beats using an array of tags and beats */
|
||||
public assignTagsToBeats = async (
|
||||
assignments: BeatsTagAssignment[]
|
||||
): Promise<CMAssignmentReturn[]> => {
|
||||
return await this.adapter.assignTagsToBeats(assignments);
|
||||
}
|
||||
};
|
||||
|
||||
private async mergeInTags(beats: CMBeat[]): Promise<CMPopulatedBeat[]> {
|
||||
/** method user to join tags to beats, thus fully populating the beats */
|
||||
private mergeInTags = async (beats: CMBeat[]): Promise<CMPopulatedBeat[]> => {
|
||||
const tagIds = flatten(beats.map(b => b.tags || []));
|
||||
const tags = await this.libs.tags.getTagsWithIds(tagIds);
|
||||
|
||||
// TODO the filter should not be needed, if the data gets into a bad state, we should error
|
||||
// and inform the user they need to delte the tag, or else we should auto delete it
|
||||
// and inform the user they need to delete the tag, or else we should auto delete it
|
||||
// https://github.com/elastic/kibana/issues/26021
|
||||
const mergedBeats: CMPopulatedBeat[] = beats.map(
|
||||
b =>
|
||||
({
|
||||
|
@ -66,5 +80,5 @@ export class BeatsLib {
|
|||
} as CMPopulatedBeat)
|
||||
);
|
||||
return mergedBeats;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,19 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore not typed yet
|
||||
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
|
||||
// @ts-ignore
|
||||
import 'ui/autoload/all';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import chrome from 'ui/chrome';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
// @ts-ignore not typed yet
|
||||
import { management } from 'ui/management';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import { uiModules } from 'ui/modules';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import routes from 'ui/routes';
|
||||
|
||||
import { INDEX_NAMES } from '../../../common/constants/index_names';
|
||||
import { getSupportedConfig } from '../../config_schemas_translations_map';
|
||||
import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter';
|
||||
|
@ -27,8 +21,13 @@ import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter';
|
|||
import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter';
|
||||
import { BeatsLib } from '../beats';
|
||||
import { ElasticsearchLib } from '../elasticsearch';
|
||||
import { FrontendDomainLibs, FrontendLibs } from '../lib';
|
||||
import { TagsLib } from '../tags';
|
||||
import { FrontendLibs } from '../types';
|
||||
import { PLUGIN } from './../../../common/constants/plugin';
|
||||
import { FrameworkLib } from './../framework';
|
||||
|
||||
// A super early spot in kibana loading that we can use to hook before most other things
|
||||
const onKibanaReady = chrome.dangerouslyGetActiveInjector;
|
||||
|
||||
export function compose(): FrontendLibs {
|
||||
const api = new AxiosRestAPIAdapter(chrome.getXsrfToken(), chrome.getBasePath());
|
||||
|
@ -40,25 +39,24 @@ export function compose(): FrontendLibs {
|
|||
tags,
|
||||
});
|
||||
|
||||
const domainLibs: FrontendDomainLibs = {
|
||||
tags,
|
||||
tokens,
|
||||
beats,
|
||||
};
|
||||
const pluginUIModule = uiModules.get('app/beats_management');
|
||||
|
||||
const framework = new KibanaFrameworkAdapter(
|
||||
pluginUIModule,
|
||||
management,
|
||||
routes,
|
||||
chrome,
|
||||
XPackInfoProvider
|
||||
const framework = new FrameworkLib(
|
||||
new KibanaFrameworkAdapter(
|
||||
PLUGIN.ID,
|
||||
management,
|
||||
routes,
|
||||
chrome.getBasePath,
|
||||
onKibanaReady,
|
||||
XPackInfoProvider,
|
||||
chrome.getUiSettingsClient()
|
||||
)
|
||||
);
|
||||
|
||||
const libs: FrontendLibs = {
|
||||
framework,
|
||||
elasticsearch: new ElasticsearchLib(esAdapter),
|
||||
...domainLibs,
|
||||
tags,
|
||||
tokens,
|
||||
beats,
|
||||
};
|
||||
return libs;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
import 'ui/autoload/all';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import { management } from 'ui/management';
|
||||
|
@ -11,21 +12,21 @@ import { management } from 'ui/management';
|
|||
import { uiModules } from 'ui/modules';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import routes from 'ui/routes';
|
||||
import { getSupportedConfig } from '../../config_schemas_translations_map';
|
||||
// @ts-ignore: path dynamic for kibana
|
||||
import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter';
|
||||
import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
|
||||
import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter';
|
||||
import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter';
|
||||
|
||||
import { BeatsLib } from '../beats';
|
||||
import { FrontendDomainLibs, FrontendLibs } from '../lib';
|
||||
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
import { getSupportedConfig } from '../../config_schemas_translations_map';
|
||||
import { FrameworkLib } from '../framework';
|
||||
import { TagsLib } from '../tags';
|
||||
import { FrontendLibs } from '../types';
|
||||
import { MemoryElasticsearchAdapter } from './../adapters/elasticsearch/memory';
|
||||
import { ElasticsearchLib } from './../elasticsearch';
|
||||
|
||||
const onKibanaReady = uiModules.get('kibana').run;
|
||||
|
||||
export function compose(
|
||||
mockIsKueryValid: (kuery: string) => boolean,
|
||||
mockKueryToEsQuery: (kuery: string) => string,
|
||||
|
@ -40,18 +41,25 @@ export function compose(
|
|||
const tokens = new MemoryTokensAdapter();
|
||||
const beats = new BeatsLib(new MemoryBeatsAdapter([]), { tags });
|
||||
|
||||
const domainLibs: FrontendDomainLibs = {
|
||||
const pluginUIModule = uiModules.get('app/beats_management');
|
||||
|
||||
const framework = new FrameworkLib(
|
||||
new KibanaFrameworkAdapter(
|
||||
pluginUIModule,
|
||||
management,
|
||||
routes,
|
||||
() => '',
|
||||
onKibanaReady,
|
||||
null,
|
||||
null
|
||||
)
|
||||
);
|
||||
const libs: FrontendLibs = {
|
||||
framework,
|
||||
elasticsearch: new ElasticsearchLib(esAdapter),
|
||||
tags,
|
||||
tokens,
|
||||
beats,
|
||||
};
|
||||
const pluginUIModule = uiModules.get('app/beats_management');
|
||||
|
||||
const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes, null, null);
|
||||
const libs: FrontendLibs = {
|
||||
...domainLibs,
|
||||
elasticsearch: new ElasticsearchLib(esAdapter),
|
||||
framework,
|
||||
};
|
||||
return libs;
|
||||
}
|
||||
|
|
40
x-pack/plugins/beats_management/public/lib/framework.ts
Normal file
40
x-pack/plugins/beats_management/public/lib/framework.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { difference, get } from 'lodash';
|
||||
import { LICENSES, LicenseType } from '../../common/constants/security';
|
||||
import { FrameworkAdapter } from './adapters/framework/adapter_types';
|
||||
|
||||
export class FrameworkLib {
|
||||
public waitUntilFrameworkReady = this.adapter.waitUntilFrameworkReady.bind(this.adapter);
|
||||
public renderUIAtPath = this.adapter.renderUIAtPath.bind(this.adapter);
|
||||
public registerManagementSection = this.adapter.registerManagementSection.bind(this.adapter);
|
||||
public registerManagementUI = this.adapter.registerManagementUI.bind(this.adapter);
|
||||
public setUISettings = this.adapter.setUISettings.bind(this.adapter);
|
||||
public getUISetting = this.adapter.getUISetting.bind(this.adapter);
|
||||
|
||||
constructor(private readonly adapter: FrameworkAdapter) {}
|
||||
|
||||
public get currentUser() {
|
||||
return this.adapter.currentUser;
|
||||
}
|
||||
|
||||
public get info() {
|
||||
return this.adapter.info;
|
||||
}
|
||||
|
||||
public licenseIsAtLeast(type: LicenseType) {
|
||||
return (
|
||||
LICENSES.indexOf(get(this.adapter.info, 'license.type', 'oss')) >= LICENSES.indexOf(type)
|
||||
);
|
||||
}
|
||||
|
||||
public currentUserHasOneOfRoles(roles: string[]) {
|
||||
// If the user has at least one of the roles requested, the returnd difference will be less
|
||||
// then the orig array size. difference only compares based on the left side arg
|
||||
return difference(roles, get<string[]>(this.currentUser, 'roles', [])).length < roles.length;
|
||||
}
|
||||
}
|
|
@ -20,8 +20,10 @@ export class TagsLib {
|
|||
public async delete(tagIds: string[]): Promise<boolean> {
|
||||
return await this.adapter.delete(tagIds);
|
||||
}
|
||||
public async getAll(): Promise<BeatTag[]> {
|
||||
return this.jsonConfigToUserYaml(await this.adapter.getAll());
|
||||
|
||||
// FIXME: This needs to be paginated https://github.com/elastic/kibana/issues/26022
|
||||
public async getAll(ESQuery?: string): Promise<BeatTag[]> {
|
||||
return this.jsonConfigToUserYaml(await this.adapter.getAll(ESQuery));
|
||||
}
|
||||
public async upsertTag(tag: BeatTag): Promise<BeatTag | null> {
|
||||
tag.id = tag.id.replace(' ', '-');
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import { IModule, IScope } from 'angular';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import React from 'react';
|
||||
import { FrameworkAdapter } from './adapters/framework/adapter_types';
|
||||
import { CMTokensAdapter } from './adapters/tokens/adapter_types';
|
||||
import { BeatsLib } from './beats';
|
||||
import { ElasticsearchLib } from './elasticsearch';
|
||||
import { FrameworkLib } from './framework';
|
||||
import { TagsLib } from './tags';
|
||||
|
||||
export interface FrontendDomainLibs {
|
||||
|
@ -20,7 +21,7 @@ export interface FrontendDomainLibs {
|
|||
|
||||
export interface FrontendLibs extends FrontendDomainLibs {
|
||||
elasticsearch: ElasticsearchLib;
|
||||
framework: FrameworkAdapter;
|
||||
framework: FrameworkLib;
|
||||
}
|
||||
|
||||
export interface YamlConfigSchema {
|
||||
|
@ -39,35 +40,11 @@ export interface YamlConfigSchema {
|
|||
parseValidResult?: (value: any) => any;
|
||||
}
|
||||
|
||||
export interface FrameworkAdapter {
|
||||
// Instance vars
|
||||
appState?: object;
|
||||
kbnVersion?: string;
|
||||
baseURLPath: string;
|
||||
registerManagementSection(pluginId: string, displayName: string, basePath: string): void;
|
||||
getDefaultUserRoles(): string[];
|
||||
// Methods
|
||||
getCurrentUser(): {
|
||||
email: string | null;
|
||||
enabled: boolean;
|
||||
full_name: string | null;
|
||||
metadata: { _reserved: true };
|
||||
roles: string[];
|
||||
scope: string[];
|
||||
username: string;
|
||||
};
|
||||
licenseExpired(): boolean;
|
||||
securityEnabled(): boolean;
|
||||
hasValidLicense(): boolean;
|
||||
setUISettings(key: string, value: any): void;
|
||||
render(component: React.ReactElement<any>): void;
|
||||
}
|
||||
|
||||
export interface FramworkAdapterConstructable {
|
||||
new (uiModule: IModule): FrameworkAdapter;
|
||||
}
|
||||
|
||||
// TODO: replace AxiosRequestConfig with something more defined
|
||||
// FIXME: replace AxiosRequestConfig with something more defined
|
||||
export type RequestConfig = AxiosRequestConfig;
|
||||
|
||||
export interface ApiAdapter {
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { first, sortByOrder } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { CMPopulatedBeat } from '../../../common/domain_types';
|
||||
|
||||
interface BeatDetailsActionSectionProps {
|
||||
beat: CMPopulatedBeat | undefined;
|
||||
}
|
||||
|
||||
export const BeatDetailsActionSection = ({ beat }: BeatDetailsActionSectionProps) => (
|
||||
<div>
|
||||
{beat ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.actionSectionTypeLabel"
|
||||
defaultMessage="Type: {beatType}."
|
||||
values={{ beatType: <strong>{beat.type}</strong> }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.actionSectionVersionLabel"
|
||||
defaultMessage="Version: {beatVersion}."
|
||||
values={{ beatVersion: <strong>{beat.version}</strong> }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{/* TODO: We need a populated field before we can run this code
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
Uptime: <strong>12min.</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem> */}
|
||||
{beat.full_tags && beat.full_tags.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.lastConfigUpdateMessage"
|
||||
defaultMessage="Last Config Update: {lastUpdateTime}."
|
||||
values={{
|
||||
lastUpdateTime: (
|
||||
<strong>
|
||||
{moment(
|
||||
first(sortByOrder(beat.full_tags, 'last_updated')).last_updated
|
||||
).fromNow()}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.beatNotFoundMessage"
|
||||
defaultMessage="Beat not found"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
|
||||
interface BeatActivityPageProps {
|
||||
libs: FrontendLibs;
|
||||
}
|
||||
|
||||
export const BeatActivityPage = (props: BeatActivityPageProps) => (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.beatActivityViewTitle"
|
||||
defaultMessage="Beat Activity View"
|
||||
/>
|
||||
</div>
|
||||
);
|
|
@ -10,21 +10,24 @@ import {
|
|||
// @ts-ignore EuiInMemoryTable typings not yet available
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { flatten, get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { TABLE_CONFIG } from '../../../common/constants';
|
||||
import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { ConnectedLink } from '../../components/connected_link';
|
||||
import { Breadcrumb } from '../../components/navigation/breadcrumb';
|
||||
import { ConnectedLink } from '../../components/navigation/connected_link';
|
||||
import { TagBadge } from '../../components/tag';
|
||||
import { ConfigView } from '../../components/tag/config_view/index';
|
||||
import { getSupportedConfig } from '../../config_schemas_translations_map';
|
||||
|
||||
interface PageProps {
|
||||
beat: CMPopulatedBeat | undefined;
|
||||
beat: CMPopulatedBeat;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
|
@ -45,12 +48,10 @@ class BeatDetailPageUi extends React.PureComponent<PageProps, PageState> {
|
|||
const { beat, intl } = props;
|
||||
if (!beat) {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.beatNotFoundErrorTitle"
|
||||
defaultMessage="Beat not found"
|
||||
/>
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.beatNotFoundErrorTitle"
|
||||
defaultMessage="Beat not found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const configurationBlocks = flatten(
|
||||
|
@ -126,6 +127,14 @@ class BeatDetailPageUi extends React.PureComponent<PageProps, PageState> {
|
|||
];
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.beatsManagement.breadcrumb.beatDetails', {
|
||||
defaultMessage: 'Beat details for: {beatId}',
|
||||
values: { beatId: beat.id },
|
||||
})}
|
||||
path={`/beat/${beat.id}/details`}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
|
@ -5,53 +5,40 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiSpacer,
|
||||
// @ts-ignore types for EuiTab not currently available
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore types for EuiTabs not currently available
|
||||
// @ts-ignore
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { first, sortByOrder } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { PrimaryLayout } from '../../components/layouts/primary';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
import { BeatDetailsActionSection } from './action_section';
|
||||
import { BeatActivityPage } from './activity';
|
||||
import { BeatDetailPage } from './detail';
|
||||
import { BeatTagsPage } from './tags';
|
||||
import { Breadcrumb } from '../../components/navigation/breadcrumb';
|
||||
import { ChildRoutes } from '../../components/navigation/child_routes';
|
||||
import { AppPageProps } from '../../frontend_types';
|
||||
|
||||
interface Match {
|
||||
params: any;
|
||||
}
|
||||
|
||||
interface BeatDetailsPageProps extends URLStateProps<AppURLState> {
|
||||
location: any;
|
||||
history: any;
|
||||
libs: FrontendLibs;
|
||||
match: Match;
|
||||
interface PageProps extends AppPageProps {
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface BeatDetailsPageState {
|
||||
interface PageState {
|
||||
beat: CMPopulatedBeat | undefined;
|
||||
beatId: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
class BeatDetailsPageComponent extends React.PureComponent<
|
||||
BeatDetailsPageProps,
|
||||
BeatDetailsPageState
|
||||
> {
|
||||
constructor(props: BeatDetailsPageProps) {
|
||||
class BeatDetailsPageComponent extends React.PureComponent<PageProps, PageState> {
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
beat: undefined,
|
||||
beatId: this.props.match.params.beatId,
|
||||
beatId: props.match.params.beatId,
|
||||
isLoading: true,
|
||||
};
|
||||
this.loadBeat();
|
||||
|
@ -64,16 +51,66 @@ class BeatDetailsPageComponent extends React.PureComponent<
|
|||
});
|
||||
};
|
||||
|
||||
public renderActionSection(beat?: CMPopulatedBeat) {
|
||||
return beat ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.actionSectionTypeLabel"
|
||||
defaultMessage="Type: {beatType}."
|
||||
values={{ beatType: <strong>{beat.type}</strong> }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.actionSectionVersionLabel"
|
||||
defaultMessage="Version: {beatVersion}."
|
||||
values={{ beatVersion: <strong>{beat.version}</strong> }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{beat.full_tags && beat.full_tags.length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.lastConfigUpdateMessage"
|
||||
defaultMessage="Last Config Update: {lastUpdateTime}."
|
||||
values={{
|
||||
lastUpdateTime: (
|
||||
<strong>
|
||||
{moment(
|
||||
first(sortByOrder(beat.full_tags, 'last_updated')).last_updated
|
||||
).fromNow()}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.beatNotFoundMessage"
|
||||
defaultMessage="Beat not found"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
const { beat } = this.state;
|
||||
let id;
|
||||
let id: string | undefined;
|
||||
let name;
|
||||
|
||||
if (beat) {
|
||||
id = beat.id;
|
||||
name = beat.name;
|
||||
}
|
||||
|
||||
const title = this.state.isLoading
|
||||
? intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.beat.loadingTitle',
|
||||
|
@ -95,77 +132,57 @@ class BeatDetailsPageComponent extends React.PureComponent<
|
|||
}
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: `/beat/${id}`,
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.beat.configTabLabel',
|
||||
defaultMessage: 'Config',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// id: `/beat/${id}/activity`,
|
||||
// name: 'Beat Activity',
|
||||
// disabled: false,
|
||||
// },
|
||||
{
|
||||
id: `/beat/${id}/tags`,
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.beat.configurationTagsTabLabel',
|
||||
defaultMessage: 'Configuration Tags',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PrimaryLayout title={title} actionSection={<BeatDetailsActionSection beat={beat} />}>
|
||||
<EuiTabs>
|
||||
{tabs.map((tab, index) => (
|
||||
<PrimaryLayout
|
||||
title={title}
|
||||
actionSection={this.renderActionSection(beat)}
|
||||
hideBreadcrumbs={this.props.libs.framework.info.k7Design}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Breadcrumb title={`Enrolled Beats`} path={`/overview/enrolled_beats`} />
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
disabled={tab.disabled}
|
||||
key={index}
|
||||
isSelected={tab.id === this.props.history.location.pathname}
|
||||
onClick={() => {
|
||||
this.props.history.push({
|
||||
pathname: tab.id,
|
||||
search: this.props.location.search,
|
||||
});
|
||||
}}
|
||||
isSelected={`/beat/${id}/details` === this.props.history.location.pathname}
|
||||
onClick={this.onTabClicked(`/beat/${id}/details`)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
<EuiSpacer size="l" />
|
||||
<Switch>
|
||||
<Route
|
||||
path="/beat/:beatId/activity"
|
||||
render={(props: any) => <BeatActivityPage libs={this.props.libs} {...props} />}
|
||||
/>
|
||||
<Route
|
||||
path="/beat/:beatId/tags"
|
||||
render={(props: any) => (
|
||||
<BeatTagsPage
|
||||
beatId={this.state.beatId}
|
||||
libs={this.props.libs}
|
||||
refreshBeat={() => this.loadBeat()}
|
||||
{...props}
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.configTabLabel"
|
||||
defaultMessage="Config"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/beat/:beatId"
|
||||
render={(props: any) => (
|
||||
<BeatDetailPage beat={this.state.beat} libs={this.props.libs} {...props} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={`/beat/${id}/tags` === this.props.history.location.pathname}
|
||||
onClick={this.onTabClicked(`/beat/${id}/tags`)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beat.configurationTagsTabLabel"
|
||||
defaultMessage="Configuration tags"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
{!this.state.beat && <div>Beat not found</div>}
|
||||
{this.state.beat && (
|
||||
<Switch>
|
||||
<ChildRoutes
|
||||
routes={this.props.routes}
|
||||
{...this.props}
|
||||
beat={this.state.beat}
|
||||
useSwitch={false}
|
||||
/>
|
||||
{id && <Route render={() => <Redirect to={`/beat/${id}/details`} />} />}
|
||||
</Switch>
|
||||
)}
|
||||
</React.Fragment>
|
||||
</PrimaryLayout>
|
||||
);
|
||||
}
|
||||
|
||||
private onTabClicked = (path: string) => {
|
||||
return () => {
|
||||
this.props.goTo(path);
|
||||
};
|
||||
};
|
||||
|
||||
private async loadBeat() {
|
||||
const { intl } = this.props;
|
||||
const { beatId } = this.props.match.params;
|
||||
|
@ -186,6 +203,5 @@ class BeatDetailsPageComponent extends React.PureComponent<
|
|||
this.setState({ beat, isLoading: false });
|
||||
}
|
||||
}
|
||||
const BeatDetailsPageUi = withUrlState<BeatDetailsPageProps>(BeatDetailsPageComponent);
|
||||
|
||||
export const BeatDetailsPage = injectI18n(BeatDetailsPageUi);
|
||||
export const BeatDetailsPage = injectI18n(BeatDetailsPageComponent);
|
||||
|
|
|
@ -5,19 +5,20 @@
|
|||
*/
|
||||
|
||||
import { EuiGlobalToastList } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { Breadcrumb } from '../../components/navigation/breadcrumb';
|
||||
import { BeatDetailTagsTable, Table } from '../../components/table';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
import { FrontendLibs } from '../../lib/types';
|
||||
|
||||
interface BeatTagsPageProps {
|
||||
beatId: string;
|
||||
beat: CMPopulatedBeat;
|
||||
libs: FrontendLibs;
|
||||
refreshBeat(): void;
|
||||
}
|
||||
|
||||
interface BeatTagsPageState {
|
||||
beat: CMPopulatedBeat | null;
|
||||
notifications: any[];
|
||||
}
|
||||
|
||||
|
@ -27,19 +28,21 @@ export class BeatTagsPage extends React.PureComponent<BeatTagsPageProps, BeatTag
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
beat: null,
|
||||
notifications: [],
|
||||
};
|
||||
}
|
||||
|
||||
public async componentWillMount() {
|
||||
await this.getBeat();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { beat } = this.state;
|
||||
const { beat } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.beatsManagement.breadcrumb.beatTags', {
|
||||
defaultMessage: 'Beat tags for: {beatId}',
|
||||
values: { beatId: beat.id },
|
||||
})}
|
||||
path={`/beat/${beat.id}/tags`}
|
||||
/>
|
||||
<Table
|
||||
hideTableControls={true}
|
||||
items={beat ? beat.full_tags : []}
|
||||
|
@ -52,16 +55,7 @@ export class BeatTagsPage extends React.PureComponent<BeatTagsPageProps, BeatTag
|
|||
dismissToast={() => this.setState({ notifications: [] })}
|
||||
toastLifeTimeMs={5000}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private getBeat = async () => {
|
||||
try {
|
||||
const beat = await this.props.libs.beats.get(this.props.beatId);
|
||||
this.setState({ beat });
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import * as React from 'react';
|
||||
import { NoDataLayout } from '../components/layouts/no_data';
|
||||
import { NoDataLayout } from '../../components/layouts/no_data';
|
||||
|
||||
export const EnforceSecurityPage = injectI18n(({ intl }) => (
|
||||
<NoDataLayout
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import * as React from 'react';
|
||||
import { NoDataLayout } from '../components/layouts/no_data';
|
||||
import { NoDataLayout } from '../../components/layouts/no_data';
|
||||
|
||||
export const InvalidLicensePage = injectI18n(({ intl }) => (
|
||||
<NoDataLayout
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import * as React from 'react';
|
||||
import { NoDataLayout } from '../components/layouts/no_data';
|
||||
import { NoDataLayout } from '../../components/layouts/no_data';
|
||||
|
||||
export const NoAccessPage = injectI18n(({ intl }) => (
|
||||
<NoDataLayout
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
export class ActivityPage extends React.PureComponent {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.activityLogsViewTitle"
|
||||
defaultMessage="activity logs view"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
EuiTab,
|
||||
// @ts-ignore
|
||||
EuiTabs,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { ConnectedLink } from '../../components/connected_link';
|
||||
import { NoDataLayout } from '../../components/layouts/no_data';
|
||||
import { PrimaryLayout } from '../../components/layouts/primary';
|
||||
import { WalkthroughLayout } from '../../components/layouts/walkthrough';
|
||||
import { RouteWithBreadcrumb } from '../../components/route_with_breadcrumb';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
import { ActivityPage } from './activity';
|
||||
import { BeatsPage } from './beats';
|
||||
import { CreateTagPageFragment } from './create_tag_fragment';
|
||||
import { EnrollBeatPage } from './enroll_fragment';
|
||||
import { FinishWalkthroughPage } from './finish_walkthrough';
|
||||
import { TagsPage } from './tags';
|
||||
|
||||
interface MainPagesProps extends URLStateProps<AppURLState> {
|
||||
libs: FrontendLibs;
|
||||
location: any;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface MainPagesState {
|
||||
enrollBeat?: {
|
||||
enrollmentToken: string;
|
||||
} | null;
|
||||
beats: CMPopulatedBeat[];
|
||||
unfilteredBeats: CMPopulatedBeat[];
|
||||
loadedBeatsAtLeastOnce: boolean;
|
||||
}
|
||||
|
||||
class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesState> {
|
||||
private mounted: boolean = false;
|
||||
|
||||
constructor(props: MainPagesProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loadedBeatsAtLeastOnce: false,
|
||||
beats: [],
|
||||
unfilteredBeats: [],
|
||||
};
|
||||
}
|
||||
public onSelectedTabChanged = (id: string) => {
|
||||
this.props.goTo(id);
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.loadBeats();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
if (
|
||||
this.state.loadedBeatsAtLeastOnce &&
|
||||
this.state.unfilteredBeats.length === 0 &&
|
||||
!this.props.location.pathname.includes('/overview/initial')
|
||||
) {
|
||||
return <Redirect to="/overview/initial/help" />;
|
||||
}
|
||||
const tabs = [
|
||||
{
|
||||
id: '/overview/beats',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beats.enrolledBeatsTabTitle"
|
||||
defaultMessage="Enrolled Beats"
|
||||
/>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// id: '/overview/activity',
|
||||
// name: 'Beats Activity',
|
||||
// disabled: false,
|
||||
// },
|
||||
{
|
||||
id: '/overview/tags',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beats.configurationTagsTabTitle"
|
||||
defaultMessage="Configuration tags"
|
||||
/>
|
||||
),
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const walkthroughSteps = [
|
||||
{
|
||||
id: '/overview/initial/beats',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.enrollBeatStepLabel',
|
||||
defaultMessage: 'Enroll Beat',
|
||||
}),
|
||||
disabled: false,
|
||||
page: EnrollBeatPage,
|
||||
},
|
||||
{
|
||||
id: '/overview/initial/tag',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.createTagStepLabel',
|
||||
defaultMessage: 'Create tag',
|
||||
}),
|
||||
disabled: false,
|
||||
page: CreateTagPageFragment,
|
||||
},
|
||||
{
|
||||
id: '/overview/initial/finish',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.finishStepLabel',
|
||||
defaultMessage: 'Finish',
|
||||
}),
|
||||
disabled: false,
|
||||
page: FinishWalkthroughPage,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.location.pathname === '/overview/initial/help') {
|
||||
return (
|
||||
<NoDataLayout
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.beatsCentralManagementTitle',
|
||||
defaultMessage: 'Beats central management (Beta)',
|
||||
})}
|
||||
actionSection={
|
||||
<ConnectedLink path="/overview/initial/beats">
|
||||
<EuiButton color="primary" fill>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.enrollBeat.enrollBeatButtonLabel"
|
||||
defaultMessage="Enroll Beat"
|
||||
/>
|
||||
</EuiButton>
|
||||
</ConnectedLink>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.enrollBeat.beatsCentralManagementDescription"
|
||||
defaultMessage="Manage your configurations in a central location."
|
||||
/>
|
||||
</p>
|
||||
</NoDataLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.location.pathname.includes('/overview/initial')) {
|
||||
return (
|
||||
<WalkthroughLayout
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.getStartedBeatsCentralManagementTitle',
|
||||
defaultMessage: 'Get started with Beats central management',
|
||||
})}
|
||||
walkthroughSteps={walkthroughSteps}
|
||||
goTo={this.props.goTo}
|
||||
activePath={this.props.location.pathname}
|
||||
>
|
||||
<Switch>
|
||||
{walkthroughSteps.map(step => (
|
||||
<Route
|
||||
path={step.id}
|
||||
render={(props: any) => (
|
||||
<step.page
|
||||
{...this.props}
|
||||
{...props}
|
||||
libs={this.props.libs}
|
||||
loadBeats={this.loadBeats}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</WalkthroughLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const renderedTabs = tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
onClick={() => this.onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === this.props.location.pathname}
|
||||
disabled={tab.disabled}
|
||||
key={index}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
));
|
||||
|
||||
return (
|
||||
<PrimaryLayout
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.beatsRouteTitle',
|
||||
defaultMessage: 'Beats',
|
||||
})}
|
||||
actionSection={
|
||||
<Switch>
|
||||
<Route
|
||||
path="/overview/beats/:action?/:enrollmentToken?"
|
||||
render={(props: any) => (
|
||||
<BeatsPage.ActionArea {...this.props} {...props} libs={this.props.libs} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/overview/tags"
|
||||
render={(props: any) => (
|
||||
<TagsPage.ActionArea {...this.props} {...props} libs={this.props.libs} />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
}
|
||||
>
|
||||
<EuiTabs>{renderedTabs}</EuiTabs>
|
||||
|
||||
<RouteWithBreadcrumb
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.beatsListRouteTitle',
|
||||
defaultMessage: 'Beats List',
|
||||
})}
|
||||
path="/overview/beats/:action?/:enrollmentToken?"
|
||||
render={(props: any) => (
|
||||
<BeatsPage
|
||||
{...this.props}
|
||||
libs={this.props.libs}
|
||||
{...props}
|
||||
loadBeats={this.loadBeats}
|
||||
beats={this.state.beats}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<RouteWithBreadcrumb
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.activityOverviewRouteTitle',
|
||||
defaultMessage: 'Activity Overview',
|
||||
})}
|
||||
path="/overview/activity"
|
||||
exact={true}
|
||||
render={(props: any) => (
|
||||
<ActivityPage {...this.props} libs={this.props.libs} {...props} />
|
||||
)}
|
||||
/>
|
||||
<RouteWithBreadcrumb
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tagsListRouteTitle',
|
||||
defaultMessage: 'Tags List',
|
||||
})}
|
||||
path="/overview/tags"
|
||||
exact={true}
|
||||
render={(props: any) => <TagsPage {...this.props} libs={this.props.libs} {...props} />}
|
||||
/>
|
||||
</PrimaryLayout>
|
||||
);
|
||||
}
|
||||
|
||||
private loadBeats = async () => {
|
||||
let query;
|
||||
if (this.props.urlState.beatsKBar) {
|
||||
query = await this.props.libs.elasticsearch.convertKueryToEsQuery(
|
||||
this.props.urlState.beatsKBar
|
||||
);
|
||||
}
|
||||
|
||||
let beats: CMPopulatedBeat[];
|
||||
let unfilteredBeats: CMPopulatedBeat[];
|
||||
try {
|
||||
[beats, unfilteredBeats] = await Promise.all([
|
||||
this.props.libs.beats.getAll(query),
|
||||
this.props.libs.beats.getAll(),
|
||||
]);
|
||||
} catch (e) {
|
||||
beats = [];
|
||||
unfilteredBeats = [];
|
||||
}
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
loadedBeatsAtLeastOnce: true,
|
||||
beats,
|
||||
unfilteredBeats,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const MainPagesUi = withUrlState<MainPagesProps>(MainPagesComponent);
|
||||
|
||||
export const MainPages = injectI18n(MainPagesUi);
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { BeatTag } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { AssignmentActionType, Table, TagsTableType } from '../../components/table';
|
||||
import { tagListAssignmentOptions } from '../../components/table/assignment_schema';
|
||||
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
|
||||
import { URLStateProps } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
|
||||
interface TagsPageProps extends URLStateProps<AppURLState> {
|
||||
libs: FrontendLibs;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface TagsPageState {
|
||||
tags: BeatTag[];
|
||||
tableRef: any;
|
||||
}
|
||||
|
||||
class TagsPageUi extends React.PureComponent<TagsPageProps, TagsPageState> {
|
||||
public static ActionArea = ({ goTo }: TagsPageProps) => (
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
goTo('/tag/create');
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.tags.addTagButtonLabel"
|
||||
defaultMessage="Add Tag"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
constructor(props: TagsPageProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tags: [],
|
||||
tableRef: React.createRef(),
|
||||
};
|
||||
|
||||
this.loadTags();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<WithKueryAutocompletion libs={this.props.libs} fieldPrefix="tag">
|
||||
{autocompleteProps => (
|
||||
<Table
|
||||
kueryBarProps={{
|
||||
...autocompleteProps,
|
||||
filterQueryDraft: 'false', // todo
|
||||
isValid: this.props.libs.elasticsearch.isKueryValid(
|
||||
this.props.urlState.tagsKBar || ''
|
||||
),
|
||||
onChange: (value: any) => this.props.setUrlState({ tagsKBar: value }),
|
||||
onSubmit: () => null, // todo
|
||||
value: this.props.urlState.tagsKBar || '',
|
||||
}}
|
||||
assignmentOptions={{
|
||||
schema: tagListAssignmentOptions,
|
||||
type: 'primary',
|
||||
items: [],
|
||||
actionHandler: this.handleTagsAction,
|
||||
}}
|
||||
ref={this.state.tableRef}
|
||||
items={this.state.tags}
|
||||
type={TagsTableType}
|
||||
/>
|
||||
)}
|
||||
</WithKueryAutocompletion>
|
||||
);
|
||||
}
|
||||
|
||||
private handleTagsAction = async (action: AssignmentActionType, payload: any) => {
|
||||
const { intl } = this.props;
|
||||
switch (action) {
|
||||
case AssignmentActionType.Delete:
|
||||
const tags = this.getSelectedTags().map((tag: BeatTag) => tag.id);
|
||||
const success = await this.props.libs.tags.delete(tags);
|
||||
if (!success) {
|
||||
alert(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tags.someTagsMightBeAssignedToBeatsTitle',
|
||||
defaultMessage:
|
||||
'Some of these tags might be assigned to beats. Please ensure tags being removed are not activly assigned',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.loadTags();
|
||||
if (this.state.tableRef && this.state.tableRef.current) {
|
||||
this.state.tableRef.current.resetSelection();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.loadTags();
|
||||
};
|
||||
|
||||
private getSelectedTags = () => {
|
||||
return this.state.tableRef.current ? this.state.tableRef.current.state.selection : [];
|
||||
};
|
||||
|
||||
private async loadTags() {
|
||||
const tags = await this.props.libs.tags.getAll();
|
||||
this.setState({
|
||||
tags,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const TagsPageWrapped = injectI18n(TagsPageUi);
|
||||
export const TagsPage = TagsPageWrapped as typeof TagsPageWrapped & {
|
||||
ActionArea: typeof TagsPageUi['ActionArea'];
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { BeatTag } from '../../../common/domain_types';
|
||||
import { Breadcrumb } from '../../components/navigation/breadcrumb';
|
||||
import { AssignmentActionType, Table, TagsTableType } from '../../components/table';
|
||||
import { tagListAssignmentOptions } from '../../components/table/assignment_schema';
|
||||
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
|
||||
import { AppPageProps } from '../../frontend_types';
|
||||
|
||||
interface PageProps extends AppPageProps {
|
||||
renderAction: (area: () => JSX.Element) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface PageState {
|
||||
tableRef: any;
|
||||
}
|
||||
|
||||
class TagsPageComponent extends React.PureComponent<PageProps, PageState> {
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tableRef: React.createRef(),
|
||||
};
|
||||
|
||||
if (props.urlState.tagsKBar) {
|
||||
props.containers.tags.reload(props.urlState.tagsKBar);
|
||||
}
|
||||
|
||||
props.renderAction(this.renderActionArea);
|
||||
}
|
||||
|
||||
public renderActionArea = () => (
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
this.props.goTo('/tag/create');
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.tags.addTagButtonLabel"
|
||||
defaultMessage="Add Tag"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.beatsManagement.breadcrumb.configurationTags', {
|
||||
defaultMessage: 'Configuration tags',
|
||||
})}
|
||||
path={`/overview/configuration_tags`}
|
||||
/>
|
||||
<WithKueryAutocompletion libs={this.props.libs} fieldPrefix="tag">
|
||||
{autocompleteProps => (
|
||||
<Table
|
||||
kueryBarProps={{
|
||||
...autocompleteProps,
|
||||
filterQueryDraft: 'false', // todo
|
||||
isValid: this.props.libs.elasticsearch.isKueryValid(
|
||||
this.props.urlState.tagsKBar || ''
|
||||
),
|
||||
onChange: (value: any) => {
|
||||
this.props.setUrlState({ tagsKBar: value });
|
||||
this.props.containers.tags.reload(this.props.urlState.tagsKBar);
|
||||
},
|
||||
onSubmit: () => null, // todo
|
||||
value: this.props.urlState.tagsKBar || '',
|
||||
}}
|
||||
assignmentOptions={{
|
||||
schema: tagListAssignmentOptions,
|
||||
type: 'primary',
|
||||
items: [],
|
||||
actionHandler: this.handleTagsAction,
|
||||
}}
|
||||
ref={this.state.tableRef}
|
||||
items={this.props.containers.tags.state.list}
|
||||
type={TagsTableType}
|
||||
/>
|
||||
)}
|
||||
</WithKueryAutocompletion>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handleTagsAction = async (action: AssignmentActionType) => {
|
||||
const { intl } = this.props;
|
||||
switch (action) {
|
||||
case AssignmentActionType.Delete:
|
||||
const tags = this.getSelectedTags().map((tag: BeatTag) => tag.id);
|
||||
const success = await this.props.containers.tags.delete(tags);
|
||||
if (!success) {
|
||||
alert(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.tags.someTagsMightBeAssignedToBeatsTitle',
|
||||
defaultMessage:
|
||||
'Some of these tags might be assigned to beats. Please ensure tags being removed are not activly assigned',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (this.state.tableRef && this.state.tableRef.current) {
|
||||
this.state.tableRef.current.resetSelection();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private getSelectedTags = () => {
|
||||
return this.state.tableRef.current ? this.state.tableRef.current.state.selection : [];
|
||||
};
|
||||
}
|
||||
|
||||
export const TagsPage = injectI18n(TagsPageComponent);
|
|
@ -14,43 +14,48 @@ import {
|
|||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { flatten, intersection, sortBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { UNIQUENESS_ENFORCING_TYPES } from 'x-pack/plugins/beats_management/common/constants';
|
||||
import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { EnrollBeat } from '../../components/enroll_beats';
|
||||
import { Breadcrumb } from '../../components/navigation/breadcrumb';
|
||||
import { BeatsTableType, Table } from '../../components/table';
|
||||
import { beatsListAssignmentOptions } from '../../components/table/assignment_schema';
|
||||
import { AssignmentActionType } from '../../components/table/table';
|
||||
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
|
||||
import { URLStateProps } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
import { EnrollBeatPage } from './enroll_fragment';
|
||||
import { AppPageProps } from '../../frontend_types';
|
||||
|
||||
interface BeatsPageProps extends URLStateProps<AppURLState> {
|
||||
interface PageProps extends AppPageProps {
|
||||
renderAction: (area: () => JSX.Element) => void;
|
||||
intl: InjectedIntl;
|
||||
libs: FrontendLibs;
|
||||
location: any;
|
||||
beats: CMPopulatedBeat[];
|
||||
loadBeats: () => any;
|
||||
}
|
||||
|
||||
interface BeatsPageState {
|
||||
interface PageState {
|
||||
notifications: any[];
|
||||
tableRef: any;
|
||||
tags: any[] | null;
|
||||
tags: BeatTag[] | null;
|
||||
}
|
||||
|
||||
interface ActionAreaProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
|
||||
libs: FrontendLibs;
|
||||
}
|
||||
class BeatsPageComponent extends React.PureComponent<PageProps, PageState> {
|
||||
private tableRef: React.RefObject<any> = React.createRef();
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
|
||||
class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
||||
public static ActionArea = (props: ActionAreaProps) => (
|
||||
this.state = {
|
||||
notifications: [],
|
||||
tags: null,
|
||||
};
|
||||
|
||||
if (props.urlState.beatsKBar) {
|
||||
props.containers.beats.reload(props.urlState.beatsKBar);
|
||||
}
|
||||
props.renderAction(this.renderActionArea);
|
||||
}
|
||||
|
||||
public renderActionArea = () => (
|
||||
<React.Fragment>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
|
@ -71,7 +76,7 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
size="s"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
props.goTo(`/overview/beats/enroll`);
|
||||
this.props.goTo(`/overview/enrolled_beats/enroll`);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -80,11 +85,14 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
/>
|
||||
</EuiButton>
|
||||
|
||||
{props.location.pathname === '/overview/beats/enroll' && (
|
||||
{this.props.location.pathname === '/overview/enrolled_beats/enroll' && (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
onClose={() => {
|
||||
props.goTo(`/overview/beats`);
|
||||
this.props.setUrlState({
|
||||
enrollmentToken: '',
|
||||
});
|
||||
this.props.goTo(`/overview/enrolled_beats`);
|
||||
}}
|
||||
style={{ width: '640px' }}
|
||||
>
|
||||
|
@ -97,31 +105,52 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EnrollBeatPage {...props} />
|
||||
<EnrollBeat
|
||||
frameworkBasePath={this.props.libs.framework.info.basePath}
|
||||
enrollmentToken={this.props.urlState.enrollmentToken}
|
||||
getBeatWithToken={this.props.containers.beats.getBeatWithToken}
|
||||
createEnrollmentToken={async () => {
|
||||
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
|
||||
this.props.setUrlState({
|
||||
enrollmentToken,
|
||||
});
|
||||
}}
|
||||
onBeatEnrolled={() => {
|
||||
this.props.setUrlState({
|
||||
enrollmentToken: '',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!this.props.urlState.enrollmentToken && (
|
||||
<React.Fragment>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
style={{ marginLeft: 10 }}
|
||||
onClick={async () => {
|
||||
this.props.goTo('/overview/enrolled_beats');
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</EuiButton>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
constructor(props: BeatsPageProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notifications: [],
|
||||
tableRef: React.createRef(),
|
||||
tags: null,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
this.props.loadBeats();
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.beatsManagement.breadcrumb.enrolledBeats', {
|
||||
defaultMessage: 'Enrolled Beats',
|
||||
})}
|
||||
path={`/overview/enrolled_beats`}
|
||||
/>
|
||||
<WithKueryAutocompletion libs={this.props.libs} fieldPrefix="beat">
|
||||
{autocompleteProps => (
|
||||
<Table
|
||||
|
@ -131,18 +160,38 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
isValid: this.props.libs.elasticsearch.isKueryValid(
|
||||
this.props.urlState.beatsKBar || ''
|
||||
), // todo check if query converts to es query correctly
|
||||
onChange: (value: any) => this.props.setUrlState({ beatsKBar: value }), // todo
|
||||
onChange: (value: any) => {
|
||||
this.props.setUrlState({ beatsKBar: value });
|
||||
this.props.containers.beats.reload(this.props.urlState.beatsKBar);
|
||||
}, // todo
|
||||
onSubmit: () => null, // todo
|
||||
value: this.props.urlState.beatsKBar || '',
|
||||
}}
|
||||
assignmentOptions={{
|
||||
items: this.filterSelectedBeatTags(),
|
||||
items: this.filterTags(this.props.containers.tags.state.list),
|
||||
schema: beatsListAssignmentOptions,
|
||||
type: 'assignment',
|
||||
actionHandler: this.handleBeatsActions,
|
||||
actionHandler: async (action: AssignmentActionType, payload: any) => {
|
||||
switch (action) {
|
||||
case AssignmentActionType.Assign:
|
||||
const status = await this.props.containers.beats.toggleTagAssignment(
|
||||
payload,
|
||||
this.getSelectedBeats()
|
||||
);
|
||||
this.notifyUpdatedTagAssociation(status, this.getSelectedBeats(), payload);
|
||||
break;
|
||||
case AssignmentActionType.Delete:
|
||||
this.props.containers.beats.deactivate(this.getSelectedBeats());
|
||||
this.notifyBeatDisenrolled(this.getSelectedBeats());
|
||||
break;
|
||||
case AssignmentActionType.Reload:
|
||||
this.props.containers.tags.reload();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}}
|
||||
items={sortBy(this.props.beats, 'id') || []}
|
||||
ref={this.state.tableRef}
|
||||
items={sortBy(this.props.containers.beats.state.list, 'id') || []}
|
||||
ref={this.tableRef}
|
||||
type={BeatsTableType}
|
||||
/>
|
||||
)}
|
||||
|
@ -152,84 +201,11 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
dismissToast={() => this.setState({ notifications: [] })}
|
||||
toastLifeTimeMs={5000}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handleBeatsActions = (action: AssignmentActionType, payload: any) => {
|
||||
switch (action) {
|
||||
case AssignmentActionType.Assign:
|
||||
this.handleBeatTagAssignment(payload);
|
||||
break;
|
||||
case AssignmentActionType.Edit:
|
||||
// TODO: navigate to edit page
|
||||
break;
|
||||
case AssignmentActionType.Delete:
|
||||
this.deleteSelected();
|
||||
break;
|
||||
case AssignmentActionType.Reload:
|
||||
this.loadTags();
|
||||
break;
|
||||
}
|
||||
|
||||
this.props.loadBeats();
|
||||
};
|
||||
|
||||
private handleBeatTagAssignment = async (tagId: string) => {
|
||||
const selected = this.getSelectedBeats();
|
||||
if (selected.some(beat => beat.full_tags.some(({ id }) => id === tagId))) {
|
||||
await this.removeTagsFromBeats(selected, tagId);
|
||||
} else {
|
||||
await this.assignTagsToBeats(selected, tagId);
|
||||
}
|
||||
};
|
||||
|
||||
private deleteSelected = async () => {
|
||||
const selected = this.getSelectedBeats();
|
||||
for (const beat of selected) {
|
||||
await this.props.libs.beats.update(beat.id, { active: false });
|
||||
}
|
||||
|
||||
this.notifyBeatUnenrolled(selected);
|
||||
|
||||
// because the compile code above has a very minor race condition, we wait,
|
||||
// the max race condition time is really 10ms but doing 100 to be safe
|
||||
setTimeout(async () => {
|
||||
await this.props.loadBeats();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
private loadTags = async () => {
|
||||
const tags = await this.props.libs.tags.getAll();
|
||||
this.setState({
|
||||
tags,
|
||||
});
|
||||
};
|
||||
|
||||
private createBeatTagAssignments = (
|
||||
beats: CMPopulatedBeat[],
|
||||
tagId: string
|
||||
): BeatsTagAssignment[] => beats.map(({ id }) => ({ beatId: id, tag: tagId }));
|
||||
|
||||
private removeTagsFromBeats = async (beats: CMPopulatedBeat[], tagId: string) => {
|
||||
if (beats.length) {
|
||||
const assignments = this.createBeatTagAssignments(beats, tagId);
|
||||
await this.props.libs.beats.removeTagsFromBeats(assignments);
|
||||
await this.refreshData();
|
||||
this.notifyUpdatedTagAssociation('remove', assignments, tagId);
|
||||
}
|
||||
};
|
||||
|
||||
private assignTagsToBeats = async (beats: CMPopulatedBeat[], tagId: string) => {
|
||||
if (beats.length) {
|
||||
const assignments = this.createBeatTagAssignments(beats, tagId);
|
||||
await this.props.libs.beats.assignTagsToBeats(assignments);
|
||||
await this.refreshData();
|
||||
this.notifyUpdatedTagAssociation('add', assignments, tagId);
|
||||
}
|
||||
};
|
||||
|
||||
private notifyBeatUnenrolled = async (beats: CMPopulatedBeat[]) => {
|
||||
private notifyBeatDisenrolled = async (beats: CMPopulatedBeat[]) => {
|
||||
const { intl } = this.props;
|
||||
let title;
|
||||
let text;
|
||||
|
@ -237,7 +213,7 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
title = intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.beatsManagement.beats.beatDisenrolledNotificationTitle',
|
||||
defaultMessage: '{firstBeatNameOrId} unenrolled',
|
||||
defaultMessage: '{firstBeatNameOrId} disenrolled',
|
||||
},
|
||||
{
|
||||
firstBeatNameOrId: `"${beats[0].name || beats[0].id}"`,
|
||||
|
@ -246,7 +222,7 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
text = intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.beatsManagement.beats.beatDisenrolledNotificationDescription',
|
||||
defaultMessage: 'Beat with ID {firstBeatId} was unenrolled.',
|
||||
defaultMessage: 'Beat with ID {firstBeatId} was disenrolled.',
|
||||
},
|
||||
{
|
||||
firstBeatId: `"${beats[0].id}"`,
|
||||
|
@ -256,7 +232,7 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
title = intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.beatsManagement.beats.disenrolledBeatsNotificationTitle',
|
||||
defaultMessage: '{beatsLength} beats unenrolled',
|
||||
defaultMessage: '{beatsLength} beats disenrolled',
|
||||
},
|
||||
{
|
||||
beatsLength: beats.length,
|
||||
|
@ -267,7 +243,7 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
this.setState({
|
||||
notifications: this.state.notifications.concat({
|
||||
color: 'warning',
|
||||
id: `unenroll_${new Date()}`,
|
||||
id: `disenroll_${new Date()}`,
|
||||
title,
|
||||
text,
|
||||
}),
|
||||
|
@ -275,13 +251,13 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
};
|
||||
|
||||
private notifyUpdatedTagAssociation = (
|
||||
action: 'add' | 'remove',
|
||||
assignments: BeatsTagAssignment[],
|
||||
action: 'added' | 'removed',
|
||||
beats: CMPopulatedBeat[],
|
||||
tag: string
|
||||
) => {
|
||||
const { intl } = this.props;
|
||||
const notificationMessage =
|
||||
action === 'remove'
|
||||
action === 'removed'
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.beatsManagement.beats.removedNotificationDescription',
|
||||
|
@ -290,8 +266,8 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
},
|
||||
{
|
||||
tag: `"${tag}"`,
|
||||
assignmentsLength: assignments.length,
|
||||
beatName: `"${this.getNameForBeatId(assignments[0].beatId)}"`,
|
||||
assignmentsLength: beats.length,
|
||||
beatName: `"${beats[0].name || beats[0].id}"`,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
|
@ -302,19 +278,19 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
},
|
||||
{
|
||||
tag: `"${tag}"`,
|
||||
assignmentsLength: assignments.length,
|
||||
beatName: `"${this.getNameForBeatId(assignments[0].beatId)}"`,
|
||||
assignmentsLength: beats.length,
|
||||
beatName: `"${beats[0].name || beats[0].id}"`,
|
||||
}
|
||||
);
|
||||
const notificationTitle =
|
||||
action === 'remove'
|
||||
action === 'removed'
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: 'xpack.beatsManagement.beats.removedNotificationTitle',
|
||||
defaultMessage: '{assignmentsLength, plural, one {Tag} other {Tags}} removed',
|
||||
},
|
||||
{
|
||||
assignmentsLength: assignments.length,
|
||||
assignmentsLength: beats.length,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
|
@ -323,9 +299,10 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
defaultMessage: '{assignmentsLength, plural, one {Tag} other {Tags}} added',
|
||||
},
|
||||
{
|
||||
assignmentsLength: assignments.length,
|
||||
assignmentsLength: beats.length,
|
||||
}
|
||||
);
|
||||
|
||||
this.setState({
|
||||
notifications: this.state.notifications.concat({
|
||||
color: 'success',
|
||||
|
@ -336,25 +313,14 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
});
|
||||
};
|
||||
|
||||
private getNameForBeatId = (beatId: string) => {
|
||||
const beat = this.props.beats.find(b => b.id === beatId);
|
||||
if (beat) {
|
||||
return beat.name;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private refreshData = async () => {
|
||||
await this.loadTags();
|
||||
await this.props.loadBeats();
|
||||
this.state.tableRef.current.setSelection(this.getSelectedBeats());
|
||||
};
|
||||
|
||||
private getSelectedBeats = (): CMPopulatedBeat[] => {
|
||||
const selectedIds = this.state.tableRef.current.state.selection.map((beat: any) => beat.id);
|
||||
if (!this.tableRef.current) {
|
||||
return [];
|
||||
}
|
||||
const selectedIds = this.tableRef.current.state.selection.map((beat: any) => beat.id);
|
||||
const beats: CMPopulatedBeat[] = [];
|
||||
selectedIds.forEach((id: any) => {
|
||||
const beat: CMPopulatedBeat | undefined = this.props.beats.find(b => b.id === id);
|
||||
const beat = this.props.containers.beats.state.list.find(b => b.id === id);
|
||||
if (beat) {
|
||||
beats.push(beat);
|
||||
}
|
||||
|
@ -362,13 +328,10 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
return beats;
|
||||
};
|
||||
|
||||
private filterSelectedBeatTags = () => {
|
||||
if (!this.state.tags) {
|
||||
return [];
|
||||
}
|
||||
private filterTags = (tags: BeatTag[]) => {
|
||||
return this.selectedBeatConfigsRequireUniqueness()
|
||||
? this.state.tags.map(this.disableTagForUniquenessEnforcement)
|
||||
: this.state.tags;
|
||||
? tags.map(this.disableTagForUniquenessEnforcement)
|
||||
: tags;
|
||||
};
|
||||
|
||||
private configBlocksRequireUniqueness = (configurationBlocks: ConfigurationBlock[]) =>
|
||||
|
@ -391,7 +354,4 @@ class BeatsPageUi extends React.PureComponent<BeatsPageProps, BeatsPageState> {
|
|||
.reduce((acc, cur) => acc || cur, false);
|
||||
}
|
||||
|
||||
const BeatsPageWrapped = injectI18n(BeatsPageUi);
|
||||
export const BeatsPage = BeatsPageWrapped as typeof BeatsPageWrapped & {
|
||||
ActionArea: typeof BeatsPageUi['ActionArea'];
|
||||
};
|
||||
export const BeatsPage = injectI18n(BeatsPageComponent);
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
// @ts-ignore types for EuiTabs not currently available
|
||||
EuiTab,
|
||||
// @ts-ignore types for EuiTab not currently available
|
||||
EuiTabs,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { Subscribe } from 'unstated';
|
||||
import { CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { PrimaryLayout } from '../../components/layouts/primary';
|
||||
import { ChildRoutes } from '../../components/navigation/child_routes';
|
||||
import { BeatsContainer } from '../../containers/beats';
|
||||
import { TagsContainer } from '../../containers/tags';
|
||||
import { withUrlState } from '../../containers/with_url_state';
|
||||
import { AppPageProps } from '../../frontend_types';
|
||||
|
||||
interface MainPagesState {
|
||||
enrollBeat?: {
|
||||
enrollmentToken: string;
|
||||
} | null;
|
||||
beats: CMPopulatedBeat[];
|
||||
loadedBeatsAtLeastOnce: boolean;
|
||||
}
|
||||
|
||||
class MainPageComponent extends React.PureComponent<AppPageProps, MainPagesState> {
|
||||
constructor(props: AppPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loadedBeatsAtLeastOnce: false,
|
||||
beats: [],
|
||||
};
|
||||
}
|
||||
public onTabClicked = (path: string) => {
|
||||
return () => {
|
||||
this.props.goTo(path);
|
||||
};
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<PrimaryLayout title="Beats" hideBreadcrumbs={this.props.libs.framework.info.k7Design}>
|
||||
{(renderAction: any) => (
|
||||
<Subscribe to={[BeatsContainer, TagsContainer]}>
|
||||
{(beats: BeatsContainer, tags: TagsContainer) => (
|
||||
<React.Fragment>
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
isSelected={`/overview/enrolled_beats` === this.props.history.location.pathname}
|
||||
onClick={this.onTabClicked(`/overview/enrolled_beats`)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beats.enrolledBeatsTabTitle"
|
||||
defaultMessage="Enrolled Beats"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={
|
||||
`/overview/configuration_tags` === this.props.history.location.pathname
|
||||
}
|
||||
onClick={this.onTabClicked(`/overview/configuration_tags`)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.beats.configurationTagsTabTitle"
|
||||
defaultMessage="Configuration tags"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
<ChildRoutes
|
||||
routes={this.props.routes}
|
||||
renderAction={renderAction}
|
||||
{...this.props}
|
||||
beatsContainer={beats}
|
||||
tagsContainer={tags}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Subscribe>
|
||||
)}
|
||||
</PrimaryLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const MainPage = withUrlState<AppPageProps>(MainPageComponent);
|
|
@ -4,35 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import 'brace/mode/yaml';
|
||||
import 'brace/theme/github';
|
||||
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { sample } from 'lodash';
|
||||
import React from 'react';
|
||||
import { UNIQUENESS_ENFORCING_TYPES } from 'x-pack/plugins/beats_management/common/constants';
|
||||
import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { PrimaryLayout } from '../../components/layouts/primary';
|
||||
import { TagEdit } from '../../components/tag';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
interface TagPageProps extends URLStateProps<AppURLState> {
|
||||
libs: FrontendLibs;
|
||||
match: any;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
import { BeatTag, CMBeat, CMPopulatedBeat } from '../../common/domain_types';
|
||||
import { PrimaryLayout } from '../components/layouts/primary';
|
||||
import { TagEdit } from '../components/tag';
|
||||
import { AppPageProps } from '../frontend_types';
|
||||
|
||||
interface TagPageState {
|
||||
showFlyout: boolean;
|
||||
attachedBeats: CMPopulatedBeat[] | null;
|
||||
tag: BeatTag;
|
||||
}
|
||||
export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageState> {
|
||||
class TagPageComponent extends React.PureComponent<
|
||||
AppPageProps & {
|
||||
intl: InjectedIntl;
|
||||
},
|
||||
TagPageState
|
||||
> {
|
||||
private mode: 'edit' | 'create' = 'create';
|
||||
constructor(props: TagPageProps) {
|
||||
constructor(props: AppPageProps & { intl: InjectedIntl }) {
|
||||
super(props);
|
||||
const randomColor = sample(
|
||||
Object.keys(euiVars)
|
||||
|
@ -82,21 +79,23 @@ export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageS
|
|||
<div>
|
||||
<TagEdit
|
||||
tag={this.state.tag}
|
||||
mode={this.mode}
|
||||
onDetachBeat={async (beatIds: string[]) => {
|
||||
await this.props.libs.beats.removeTagsFromBeats(
|
||||
beatIds.map(id => {
|
||||
return { beatId: id, tag: this.state.tag.id };
|
||||
})
|
||||
);
|
||||
await this.loadAttachedBeats();
|
||||
}}
|
||||
onDetachBeat={
|
||||
this.mode === 'edit'
|
||||
? async (beatIds: string[]) => {
|
||||
await this.props.containers.beats.removeTagsFromBeats(
|
||||
beatIds,
|
||||
this.state.tag.id
|
||||
);
|
||||
await this.loadAttachedBeats();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onTagChange={(field: string, value: string | number) =>
|
||||
this.setState(oldState => ({
|
||||
tag: { ...oldState.tag, [field]: value },
|
||||
}))
|
||||
}
|
||||
attachedBeats={this.state.attachedBeats}
|
||||
attachedBeats={this.state.attachedBeats as CMBeat[]}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
|
@ -117,7 +116,7 @@ export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageS
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => this.props.goTo('/overview/tags')}>
|
||||
<EuiButtonEmpty onClick={() => this.props.goTo('/overview/configuration_tags')}>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.tag.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
|
@ -143,7 +142,7 @@ export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageS
|
|||
private loadTag = async () => {
|
||||
const tags = await this.props.libs.tags.getTagsWithIds([this.props.match.params.tagid]);
|
||||
if (tags.length === 0) {
|
||||
// TODO do something to error
|
||||
// TODO do something to error https://github.com/elastic/kibana/issues/26023
|
||||
}
|
||||
this.setState({
|
||||
tag: tags[0],
|
||||
|
@ -159,13 +158,12 @@ export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageS
|
|||
};
|
||||
private saveTag = async () => {
|
||||
await this.props.libs.tags.upsertTag(this.state.tag as BeatTag);
|
||||
this.props.goTo(`/overview/tags`);
|
||||
this.props.goTo(`/overview/configuration_tags`);
|
||||
};
|
||||
private getNumExclusiveConfigurationBlocks = () =>
|
||||
this.state.tag.configuration_blocks
|
||||
.map(({ type }) => UNIQUENESS_ENFORCING_TYPES.some(uniqueType => uniqueType === type))
|
||||
.reduce((acc, cur) => (cur ? acc + 1 : acc), 0);
|
||||
}
|
||||
export const TagPageUi = withUrlState<TagPageProps>(TagPageComponent);
|
||||
|
||||
export const TagPage = injectI18n(TagPageUi);
|
||||
export const TagPage = injectI18n(TagPageComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import { EnrollBeat } from '../../../components/enroll_beats';
|
||||
import { AppPageProps } from '../../../frontend_types';
|
||||
|
||||
interface ComponentState {
|
||||
readyToContinue: boolean;
|
||||
}
|
||||
|
||||
export class BeatsInitialEnrollmentPage extends Component<AppPageProps, ComponentState> {
|
||||
constructor(props: AppPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
readyToContinue: false,
|
||||
};
|
||||
}
|
||||
|
||||
public onBeatEnrolled = () => {
|
||||
this.setState({
|
||||
readyToContinue: true,
|
||||
});
|
||||
};
|
||||
|
||||
public createEnrollmentToken = async () => {
|
||||
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
|
||||
this.props.setUrlState({
|
||||
enrollmentToken,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EnrollBeat
|
||||
frameworkBasePath={this.props.libs.framework.info.basePath}
|
||||
enrollmentToken={this.props.urlState.enrollmentToken || ''}
|
||||
getBeatWithToken={this.props.libs.beats.getBeatWithToken}
|
||||
createEnrollmentToken={this.createEnrollmentToken}
|
||||
onBeatEnrolled={this.onBeatEnrolled}
|
||||
/>
|
||||
{this.state.readyToContinue && (
|
||||
<React.Fragment>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
style={{ marginLeft: 10 }}
|
||||
onClick={async () => {
|
||||
this.props.goTo('/walkthrough/initial/tag');
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</EuiButton>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,19 +6,23 @@
|
|||
import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { BeatTag, CMBeat } from '../../../common/domain_types';
|
||||
import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
interface PageProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
|
||||
loadBeats: any;
|
||||
libs: FrontendLibs;
|
||||
intl: InjectedIntl;
|
||||
import { CMPopulatedBeat } from '../../../../common/domain_types';
|
||||
import { AppPageProps } from '../../../frontend_types';
|
||||
|
||||
interface PageState {
|
||||
assigned: boolean;
|
||||
}
|
||||
export class FinishWalkthrough extends React.Component<PageProps, any> {
|
||||
constructor(props: PageProps) {
|
||||
class FinishWalkthrough extends React.Component<
|
||||
AppPageProps & {
|
||||
intl: InjectedIntl;
|
||||
},
|
||||
PageState
|
||||
> {
|
||||
constructor(
|
||||
props: AppPageProps & {
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -28,8 +32,6 @@ export class FinishWalkthrough extends React.Component<PageProps, any> {
|
|||
|
||||
public componentDidMount() {
|
||||
setTimeout(async () => {
|
||||
await this.props.loadBeats();
|
||||
|
||||
const done = await this.assignTagToBeat();
|
||||
|
||||
if (done) {
|
||||
|
@ -72,7 +74,7 @@ export class FinishWalkthrough extends React.Component<PageProps, any> {
|
|||
fill
|
||||
disabled={!this.state.assigned}
|
||||
onClick={async () => {
|
||||
goTo('/overview/beats');
|
||||
goTo('/overview/enrolled_beats');
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -88,9 +90,6 @@ export class FinishWalkthrough extends React.Component<PageProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
private createBeatTagAssignments = (beats: CMBeat[], tag: BeatTag): BeatsTagAssignment[] =>
|
||||
beats.map(({ id }) => ({ beatId: id, tag: tag.id }));
|
||||
|
||||
private assignTagToBeat = async () => {
|
||||
const { intl } = this.props;
|
||||
if (!this.props.urlState.enrollmentToken) {
|
||||
|
@ -119,10 +118,11 @@ export class FinishWalkthrough extends React.Component<PageProps, any> {
|
|||
})
|
||||
);
|
||||
}
|
||||
const tags = await this.props.libs.tags.getTagsWithIds([this.props.urlState.createdTag]);
|
||||
|
||||
const assignments = this.createBeatTagAssignments([beat], tags[0]);
|
||||
await this.props.libs.beats.assignTagsToBeats(assignments);
|
||||
await this.props.containers.beats.assignTagsToBeats(
|
||||
[beat as CMPopulatedBeat],
|
||||
this.props.urlState.createdTag
|
||||
);
|
||||
this.props.setUrlState({
|
||||
createdTag: '',
|
||||
enrollmentToken: '',
|
||||
|
@ -131,6 +131,4 @@ export class FinishWalkthrough extends React.Component<PageProps, any> {
|
|||
};
|
||||
}
|
||||
|
||||
const FinishWalkthroughPageUi = withUrlState<PageProps>(FinishWalkthrough);
|
||||
|
||||
export const FinishWalkthroughPage = injectI18n(FinishWalkthroughPageUi);
|
||||
export const FinishWalkthroughPage = injectI18n(FinishWalkthrough);
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import { NoDataLayout } from '../../../components/layouts/no_data';
|
||||
import { WalkthroughLayout } from '../../../components/layouts/walkthrough';
|
||||
import { ChildRoutes } from '../../../components/navigation/child_routes';
|
||||
import { ConnectedLink } from '../../../components/navigation/connected_link';
|
||||
import { AppPageProps } from '../../../frontend_types';
|
||||
|
||||
class InitialWalkthroughPageComponent extends Component<
|
||||
AppPageProps & {
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
> {
|
||||
public render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
if (this.props.location.pathname === '/walkthrough/initial') {
|
||||
return (
|
||||
<NoDataLayout
|
||||
title="Beats central management"
|
||||
actionSection={
|
||||
<ConnectedLink path="/walkthrough/initial/beat">
|
||||
<EuiButton color="primary" fill>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.enrollBeat.enrollBeatButtonLabel"
|
||||
defaultMessage="Enroll Beat"
|
||||
/>{' '}
|
||||
</EuiButton>
|
||||
</ConnectedLink>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.beatsManagement.enrollBeat.beatsCentralManagementDescription"
|
||||
defaultMessage="Manage your configurations in a central location."
|
||||
/>
|
||||
</p>
|
||||
</NoDataLayout>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WalkthroughLayout
|
||||
title={intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.getStartedBeatsCentralManagementTitle',
|
||||
defaultMessage: 'Get started with Beats central management',
|
||||
})}
|
||||
walkthroughSteps={[
|
||||
{
|
||||
id: '/walkthrough/initial/beat',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.enrollBeatStepLabel',
|
||||
defaultMessage: 'Enroll Beat',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: '/walkthrough/initial/tag',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.createTagStepLabel',
|
||||
defaultMessage: 'Create tag',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: '/walkthrough/initial/finish',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.enrollBeat.finishStepLabel',
|
||||
defaultMessage: 'Finish',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
goTo={() => {
|
||||
// FIXME implament goto
|
||||
}}
|
||||
activePath={this.props.location.pathname}
|
||||
>
|
||||
<ChildRoutes routes={this.props.routes} {...this.props} />
|
||||
</WalkthroughLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const InitialWalkthroughPage = injectI18n(InitialWalkthroughPageComponent);
|
|
@ -3,36 +3,22 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import { BeatTag } from '../../../../common/domain_types';
|
||||
import { TagEdit } from '../../../components/tag/tag_edit';
|
||||
import { AppPageProps } from '../../../frontend_types';
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import 'brace/mode/yaml';
|
||||
|
||||
import 'brace/theme/github';
|
||||
import React from 'react';
|
||||
import { BeatTag } from '../../../common/domain_types';
|
||||
import { AppURLState } from '../../app';
|
||||
import { TagEdit } from '../../components/tag';
|
||||
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
|
||||
import { FrontendLibs } from '../../lib/lib';
|
||||
|
||||
interface TagPageProps extends URLStateProps<AppURLState> {
|
||||
libs: FrontendLibs;
|
||||
match: any;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface TagPageState {
|
||||
showFlyout: boolean;
|
||||
interface PageState {
|
||||
tag: BeatTag;
|
||||
}
|
||||
|
||||
class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState> {
|
||||
private mode: 'edit' | 'create' = 'create';
|
||||
constructor(props: TagPageProps) {
|
||||
export class InitialTagPage extends Component<AppPageProps, PageState> {
|
||||
constructor(props: AppPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showFlyout: false,
|
||||
tag: {
|
||||
id: props.urlState.createdTag ? props.urlState.createdTag : '',
|
||||
color: '#DD0A73',
|
||||
|
@ -42,7 +28,6 @@ class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState>
|
|||
};
|
||||
|
||||
if (props.urlState.createdTag) {
|
||||
this.mode = 'edit';
|
||||
this.loadTag();
|
||||
}
|
||||
}
|
||||
|
@ -52,24 +37,12 @@ class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState>
|
|||
<React.Fragment>
|
||||
<TagEdit
|
||||
tag={this.state.tag}
|
||||
mode={this.mode}
|
||||
onDetachBeat={(beatIds: string[]) => {
|
||||
this.props.libs.beats.removeTagsFromBeats(
|
||||
beatIds.map(id => {
|
||||
return { beatId: id, tag: this.state.tag.id };
|
||||
})
|
||||
);
|
||||
}}
|
||||
onTagChange={(field: string, value: string | number) =>
|
||||
this.setState(oldState => ({
|
||||
tag: { ...oldState.tag, [field]: value },
|
||||
}))
|
||||
}
|
||||
attachedBeats={null}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
|
@ -102,12 +75,10 @@ class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState>
|
|||
};
|
||||
|
||||
private saveTag = async () => {
|
||||
const { intl } = this.props;
|
||||
const newTag = await this.props.libs.tags.upsertTag(this.state.tag as BeatTag);
|
||||
if (!newTag) {
|
||||
return alert(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.beatsManagement.createTag.errorSavingTagTitle',
|
||||
i18n.translate('xpack.beatsManagement.createTag.errorSavingTagTitle', {
|
||||
defaultMessage: 'error saving tag',
|
||||
})
|
||||
);
|
||||
|
@ -115,9 +86,6 @@ class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState>
|
|||
this.props.setUrlState({
|
||||
createdTag: newTag.id,
|
||||
});
|
||||
this.props.goTo(`/overview/initial/finish`);
|
||||
this.props.goTo(`/walkthrough/initial/finish`);
|
||||
};
|
||||
}
|
||||
const CreateTagPageFragmentUi = withUrlState<TagPageProps>(CreateTagFragment);
|
||||
|
||||
export const CreateTagPageFragment = injectI18n(CreateTagPageFragmentUi);
|
|
@ -4,103 +4,135 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { Header } from './components/layouts/header';
|
||||
import { BreadcrumbConsumer, RouteWithBreadcrumb } from './components/route_with_breadcrumb';
|
||||
import { FrontendLibs } from './lib/lib';
|
||||
import { BeatDetailsPage } from './pages/beat';
|
||||
import { EnforceSecurityPage } from './pages/enforce_security';
|
||||
import { InvalidLicensePage } from './pages/invalid_license';
|
||||
import { MainPages } from './pages/main';
|
||||
import { NoAccessPage } from './pages/no_access';
|
||||
import { TagPage } from './pages/tag';
|
||||
import { get } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { Loading } from './components/loading';
|
||||
import { ChildRoutes } from './components/navigation/child_routes';
|
||||
import { BeatsContainer } from './containers/beats';
|
||||
import { TagsContainer } from './containers/tags';
|
||||
import { URLStateProps, WithURLState } from './containers/with_url_state';
|
||||
import { FrontendLibs } from './lib/types';
|
||||
import { RouteTreeBuilder } from './utils/page_loader/page_loader';
|
||||
|
||||
export const PageRouter: React.SFC<{ libs: FrontendLibs }> = ({ libs }) => {
|
||||
return (
|
||||
<HashRouter basename="/management/beats_management">
|
||||
<div>
|
||||
<BreadcrumbConsumer>
|
||||
{({ breadcrumbs }) => (
|
||||
<Header
|
||||
breadcrumbs={[
|
||||
{
|
||||
href: '#/management',
|
||||
text: i18n.translate('xpack.beatsManagement.router.managementTitle', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
},
|
||||
{
|
||||
href: '#/management/beats_management',
|
||||
text: i18n.translate('xpack.beatsManagement.router.beatsTitle', {
|
||||
defaultMessage: 'Beats',
|
||||
}),
|
||||
},
|
||||
...breadcrumbs,
|
||||
]}
|
||||
// See ./utils/page_loader/readme.md for details on how this works
|
||||
// suffice to to say it dynamicly creates routes and pages based on the filesystem
|
||||
// This is to ensure that the patterns are followed and types assured
|
||||
// @ts-ignore
|
||||
const requirePages = require.context('./pages', true, /\.tsx$/);
|
||||
const routeTreeBuilder = new RouteTreeBuilder(requirePages);
|
||||
const routesFromFilesystem = routeTreeBuilder.routeTreeFromPaths(requirePages.keys(), {
|
||||
'/tag': ['action', 'tagid?'],
|
||||
'/beat': ['beatId'],
|
||||
});
|
||||
|
||||
interface RouterProps {
|
||||
libs: FrontendLibs;
|
||||
tagsContainer: TagsContainer;
|
||||
beatsContainer: BeatsContainer;
|
||||
}
|
||||
interface RouterState {
|
||||
loadingStatus: 'loading' | 'loaded:empty' | 'loaded';
|
||||
}
|
||||
|
||||
export class AppRouter extends Component<RouterProps, RouterState> {
|
||||
constructor(props: RouterProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loadingStatus: 'loading',
|
||||
};
|
||||
}
|
||||
|
||||
public async componentWillMount() {
|
||||
if (this.state.loadingStatus === 'loading') {
|
||||
await this.props.beatsContainer.reload();
|
||||
await this.props.tagsContainer.reload();
|
||||
|
||||
const countOfEverything =
|
||||
this.props.beatsContainer.state.list.length + this.props.tagsContainer.state.list.length;
|
||||
|
||||
this.setState({
|
||||
loadingStatus: countOfEverything > 0 ? 'loaded' : 'loaded:empty',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.loadingStatus === 'loading') {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Redirects mapping */}
|
||||
<Switch>
|
||||
{/* License check (UI displays when license exists but is expired) */}
|
||||
{get(this.props.libs.framework.info, 'license.expired', true) && (
|
||||
<Route
|
||||
render={props =>
|
||||
!props.location.pathname.includes('/error') ? (
|
||||
<Redirect to="/error/invalid_license" />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</BreadcrumbConsumer>
|
||||
<Switch>
|
||||
{libs.framework.licenseExpired() && <Route render={() => <InvalidLicensePage />} />}
|
||||
{!libs.framework.securityEnabled() && <Route render={() => <EnforceSecurityPage />} />}
|
||||
{!libs.framework.getCurrentUser() ||
|
||||
(!libs.framework.getCurrentUser().roles.includes('beats_admin') &&
|
||||
!libs.framework
|
||||
.getDefaultUserRoles()
|
||||
.some(r => libs.framework.getCurrentUser().roles.includes(r)) && (
|
||||
<Route render={() => <NoAccessPage />} />
|
||||
))}
|
||||
<Route
|
||||
path="/"
|
||||
exact={true}
|
||||
render={() => <Redirect from="/" exact={true} to="/overview/beats" />}
|
||||
/>
|
||||
<Route path="/overview" render={(props: any) => <MainPages {...props} libs={libs} />} />
|
||||
<RouteWithBreadcrumb
|
||||
title={params => {
|
||||
return i18n.translate('xpack.beatsManagement.router.beatTitle', {
|
||||
defaultMessage: 'Beats: {beatId}',
|
||||
values: { beatId: params.beatId },
|
||||
});
|
||||
}}
|
||||
parentBreadcrumbs={[
|
||||
{
|
||||
text: i18n.translate('xpack.beatsManagement.router.beatsListTitle', {
|
||||
defaultMessage: 'Beats List',
|
||||
}),
|
||||
href: '#/management/beats_management/overview/beats',
|
||||
},
|
||||
]}
|
||||
path="/beat/:beatId"
|
||||
render={(props: any) => <BeatDetailsPage {...props} libs={libs} />}
|
||||
/>
|
||||
<RouteWithBreadcrumb
|
||||
title={params => {
|
||||
if (params.action === 'create') {
|
||||
return i18n.translate('xpack.beatsManagement.router.createTagTitle', {
|
||||
defaultMessage: 'Create Tag',
|
||||
});
|
||||
|
||||
{/* Ensure security is eanabled for elastic and kibana */}
|
||||
{!get(this.props.libs.framework.info, 'security.enabled', true) && (
|
||||
<Route
|
||||
render={props =>
|
||||
!props.location.pathname.includes('/error') ? (
|
||||
<Redirect to="/error/enforce_security" />
|
||||
) : null
|
||||
}
|
||||
return i18n.translate('xpack.beatsManagement.router.tagTitle', {
|
||||
defaultMessage: 'Tag: {tagId}',
|
||||
values: { tagId: params.tagid },
|
||||
});
|
||||
}}
|
||||
parentBreadcrumbs={[
|
||||
{
|
||||
text: i18n.translate('xpack.beatsManagement.router.tagsListTitle', {
|
||||
defaultMessage: 'Tags List',
|
||||
}),
|
||||
href: '#/management/beats_management/overview/tags',
|
||||
},
|
||||
]}
|
||||
path="/tag/:action/:tagid?"
|
||||
render={(props: any) => <TagPage {...props} libs={libs} />}
|
||||
/>
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Make sure the user has correct permissions */}
|
||||
{!this.props.libs.framework.currentUserHasOneOfRoles(
|
||||
['beats_admin'].concat(this.props.libs.framework.info.settings.defaultUserRoles)
|
||||
) && (
|
||||
<Route
|
||||
render={props =>
|
||||
!props.location.pathname.includes('/error') ? (
|
||||
<Redirect to="/error/no_access" />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* If there are no beats or tags yet, redirect to the walkthrough */}
|
||||
{this.state.loadingStatus === 'loaded:empty' && (
|
||||
<Route
|
||||
render={props =>
|
||||
!props.location.pathname.includes('/walkthrough') ? (
|
||||
<Redirect to="/walkthrough/initial" />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* This app does not make use of a homepage. The mainpage is overview/enrolled_beats */}
|
||||
<Route path="/" exact={true} render={() => <Redirect to="/overview/enrolled_beats" />} />
|
||||
</Switch>
|
||||
</div>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Render routes from the FS */}
|
||||
<WithURLState>
|
||||
{(URLProps: URLStateProps) => (
|
||||
<ChildRoutes
|
||||
routes={routesFromFilesystem}
|
||||
{...URLProps}
|
||||
{...{
|
||||
libs: this.props.libs,
|
||||
containers: {
|
||||
beats: this.props.beatsContainer,
|
||||
tags: this.props.tagsContainer,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</WithURLState>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RouteTreeBuilder routeTreeFromPaths Should create a route tree 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/tag",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat/detail",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat/tags",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/enforce_security",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/invalid_license",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/no_access",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview/enrolled_beats",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview/tag_configurations",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/beat",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/finish",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/tag",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "*",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`RouteTreeBuilder routeTreeFromPaths Should create a route tree, with top level route having params 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/tag/:action/:tagid?",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat/detail",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/beat/tags",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/enforce_security",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/invalid_license",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/error/no_access",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview/enrolled_beats",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/overview/tag_configurations",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial",
|
||||
"routes": Array [
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/beat",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/finish",
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "/walkthrough/initial/tag",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"component": null,
|
||||
"path": "*",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RouteTreeBuilder } from './page_loader';
|
||||
|
||||
const pages = [
|
||||
'./_404.tsx',
|
||||
'./beat/detail.tsx',
|
||||
'./beat/index.tsx',
|
||||
'./beat/tags.tsx',
|
||||
'./error/enforce_security.tsx',
|
||||
'./error/invalid_license.tsx',
|
||||
'./error/no_access.tsx',
|
||||
'./overview/enrolled_beats.tsx',
|
||||
'./overview/index.tsx',
|
||||
'./overview/tag_configurations.tsx',
|
||||
'./tag.tsx',
|
||||
'./walkthrough/initial/beat.tsx',
|
||||
'./walkthrough/initial/finish.tsx',
|
||||
'./walkthrough/initial/index.tsx',
|
||||
'./walkthrough/initial/tag.tsx',
|
||||
];
|
||||
|
||||
describe('RouteTreeBuilder', () => {
|
||||
describe('routeTreeFromPaths', () => {
|
||||
it('Should fail to create a route tree due to no exported *Page component', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testComponent: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
|
||||
expect(() => {
|
||||
treeBuilder.routeTreeFromPaths(pages);
|
||||
}).toThrowError(/in the pages folder does not include an exported/);
|
||||
});
|
||||
|
||||
it('Should create a route tree', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
|
||||
let tree;
|
||||
expect(() => {
|
||||
tree = treeBuilder.routeTreeFromPaths(pages);
|
||||
}).not.toThrow();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Should fail to create a route tree due to no exported custom *Component component', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testComponent: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire, /Component$/);
|
||||
|
||||
expect(() => {
|
||||
treeBuilder.routeTreeFromPaths(pages);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('Should create a route tree, with top level route having params', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
const tree = treeBuilder.routeTreeFromPaths(pages, {
|
||||
'/tag': ['action', 'tagid?'],
|
||||
});
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Should create a route tree, with a nested route having params', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
const tree = treeBuilder.routeTreeFromPaths(pages, {
|
||||
'/beat': ['beatId'],
|
||||
});
|
||||
expect(tree[1].path).toEqual('/beat/:beatId');
|
||||
});
|
||||
});
|
||||
it('Should create a route tree, with a deep nested route having params', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
const tree = treeBuilder.routeTreeFromPaths(pages, {
|
||||
'/beat': ['beatId'],
|
||||
'/beat/detail': ['other'],
|
||||
});
|
||||
expect(tree[1].path).toEqual('/beat/:beatId');
|
||||
expect(tree[1].routes![0].path).toEqual('/beat/:beatId/detail/:other');
|
||||
expect(tree[1].routes![1].path).toEqual('/beat/:beatId/tags');
|
||||
});
|
||||
it('Should throw an error on invalid mapped path', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
expect(() => {
|
||||
treeBuilder.routeTreeFromPaths(pages, {
|
||||
'/non-existant-path': ['beatId'],
|
||||
});
|
||||
}).toThrowError(/Invalid overrideMap provided to 'routeTreeFromPaths', \/non-existant-path /);
|
||||
});
|
||||
it('Should rended 404.tsx as a 404 route not /404', () => {
|
||||
const mockRequire = jest.fn(path => ({
|
||||
path,
|
||||
testPage: null,
|
||||
}));
|
||||
|
||||
const treeBuilder = new RouteTreeBuilder(mockRequire);
|
||||
const tree = treeBuilder.routeTreeFromPaths(pages);
|
||||
const firstPath = tree[0].path;
|
||||
const lastPath = tree[tree.length - 1].path;
|
||||
|
||||
expect(firstPath).not.toBe('/_404');
|
||||
expect(lastPath).toBe('*');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { difference, flatten, last } from 'lodash';
|
||||
|
||||
interface PathTree {
|
||||
[path: string]: string[];
|
||||
}
|
||||
export interface RouteConfig {
|
||||
path: string;
|
||||
component: React.ComponentType<any>;
|
||||
routes?: RouteConfig[];
|
||||
}
|
||||
|
||||
interface RouteParamsMap {
|
||||
[path: string]: string[];
|
||||
}
|
||||
|
||||
export class RouteTreeBuilder {
|
||||
constructor(
|
||||
private readonly requireWithContext: any,
|
||||
private readonly pageComponentPattern: RegExp = /Page$/
|
||||
) {}
|
||||
|
||||
public routeTreeFromPaths(paths: string[], mapParams: RouteParamsMap = {}): RouteConfig[] {
|
||||
const pathTree = this.buildTree('./', paths);
|
||||
const allRoutes = Object.keys(pathTree).reduce((routes: any[], filePath) => {
|
||||
if (pathTree[filePath].includes('index.tsx')) {
|
||||
routes.push(this.buildRouteWithChildren(filePath, pathTree[filePath], mapParams));
|
||||
} else {
|
||||
routes.concat(
|
||||
pathTree[filePath].map(file => routes.push(this.buildRoute(filePath, file, mapParams)))
|
||||
);
|
||||
}
|
||||
|
||||
return routes;
|
||||
}, []);
|
||||
// Check that no overide maps are ignored due to being invalid
|
||||
const flatRoutes = this.flatpackRoutes(allRoutes);
|
||||
const mappedPaths = Object.keys(mapParams);
|
||||
const invalidOverrides = difference(mappedPaths, flatRoutes);
|
||||
if (invalidOverrides.length > 0 && flatRoutes.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid overrideMap provided to 'routeTreeFromPaths', ${
|
||||
invalidOverrides[0]
|
||||
} is not a valid route. Only the following are: ${flatRoutes.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// 404 route MUST be last or it gets used first in a switch
|
||||
return allRoutes.sort((a: RouteConfig) => {
|
||||
return a.path === '*' ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
private flatpackRoutes(arr: RouteConfig[], pre: string = ''): string[] {
|
||||
return flatten(
|
||||
[].concat.apply(
|
||||
[],
|
||||
arr.map(item => {
|
||||
const path = (pre + item.path).trim();
|
||||
|
||||
// The flattened route based on files without params added
|
||||
const route = item.path.includes('/:')
|
||||
? item.path
|
||||
.split('/')
|
||||
.filter(s => s.charAt(0) !== ':')
|
||||
.join('/')
|
||||
: item.path;
|
||||
return item.routes ? [route, this.flatpackRoutes(item.routes, path)] : route;
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private buildRouteWithChildren(dir: string, files: string[], mapParams: RouteParamsMap) {
|
||||
const childFiles = files.filter(f => f !== 'index.tsx');
|
||||
const parentConfig = this.buildRoute(dir, 'index.tsx', mapParams);
|
||||
parentConfig.routes = childFiles.map(cf => this.buildRoute(dir, cf, mapParams));
|
||||
return parentConfig;
|
||||
}
|
||||
|
||||
private buildRoute(dir: string, file: string, mapParams: RouteParamsMap): RouteConfig {
|
||||
// Remove the file extension as we dont want that in the URL... also index resolves to parent route
|
||||
// so remove that... e.g. /beats/index is not the url we want, /beats should resolve to /beats/index
|
||||
// just like how files resolve in node
|
||||
const filePath = `${mapParams[dir] || dir}${file.replace('.tsx', '')}`.replace('/index', '');
|
||||
const page = this.requireWithContext(`.${dir}${file}`);
|
||||
const cleanDir = dir.replace(/\/$/, '');
|
||||
|
||||
// Make sure the expored variable name matches a pattern. By default it will choose the first
|
||||
// exported variable that matches *Page
|
||||
const componentExportName = Object.keys(page).find(varName =>
|
||||
this.pageComponentPattern.test(varName)
|
||||
);
|
||||
|
||||
if (!componentExportName) {
|
||||
throw new Error(
|
||||
`${dir}${file} in the pages folder does not include an exported \`${this.pageComponentPattern.toString()}\` component`
|
||||
);
|
||||
}
|
||||
|
||||
// _404 route is special and maps to a 404 page
|
||||
if (filePath === '/_404') {
|
||||
return {
|
||||
path: '*',
|
||||
component: page[componentExportName],
|
||||
};
|
||||
}
|
||||
|
||||
// mapped route has a parent with mapped params, so we map it here too
|
||||
// e.g. /beat has a beatid param, so /beat/detail, a child of /beat
|
||||
// should also have that param resulting in /beat/:beatid/detail/:other
|
||||
if (mapParams[cleanDir] && filePath !== cleanDir) {
|
||||
const dirWithParams = `${cleanDir}/:${mapParams[cleanDir].join('/:')}`;
|
||||
const path = `${dirWithParams}/${file.replace('.tsx', '')}${
|
||||
mapParams[filePath] ? '/:' : ''
|
||||
}${(mapParams[filePath] || []).join('/:')}`;
|
||||
return {
|
||||
path,
|
||||
component: page[componentExportName],
|
||||
};
|
||||
}
|
||||
|
||||
// route matches a mapped param exactly
|
||||
// e.g. /beat has a beatid param, so it becomes /beat/:beatid
|
||||
if (mapParams[filePath]) {
|
||||
return {
|
||||
path: `${filePath}/:${mapParams[filePath].join('/:')}`,
|
||||
component: page[componentExportName],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
component: page[componentExportName],
|
||||
};
|
||||
}
|
||||
|
||||
// Build tree recursively
|
||||
private buildTree(basePath: string, paths: string[]): PathTree {
|
||||
return paths.reduce(
|
||||
(dir: any, p) => {
|
||||
const path = {
|
||||
dir:
|
||||
p
|
||||
.replace(basePath, '/') // make path absolute
|
||||
.split('/')
|
||||
.slice(0, -1) // remove file from path
|
||||
.join('/')
|
||||
.replace(/^\/\//, '') + '/', // should end in a slash but not be only //
|
||||
file: last(p.split('/')),
|
||||
};
|
||||
// take each, remove the file name
|
||||
|
||||
if (dir[path.dir]) {
|
||||
dir[path.dir].push(path.file);
|
||||
} else {
|
||||
dir[path.dir] = [path.file];
|
||||
}
|
||||
return dir;
|
||||
},
|
||||
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Page loader
|
||||
|
||||
Routing in React is not easy, nether is ensuring a clean and simple api within pages.
|
||||
This solves for both without massive config files. It also ensure URL paths match our files to make things easier to find
|
||||
|
||||
It works like this...
|
||||
|
||||
```ts
|
||||
// Create a webpack context, ensureing all pages in the pages dir are included in the build
|
||||
const requirePages = require.context('./pages', true, /\.tsx$/);
|
||||
// Pass the context based require into the RouteTreeBuilder for require the files as-needed
|
||||
const routeTreeBuilder = new RouteTreeBuilder(requirePages);
|
||||
// turn the array of file paths from the require context into a nested tree of routes based on folder structure
|
||||
const routesFromFilesystem = routeTreeBuilder.routeTreeFromPaths(requirePages.keys(), {
|
||||
'/tag': ['action', 'tagid?'], // add params to a page. In this case /tag turns into /tag/:action/:tagid?
|
||||
'/beat': ['beatId'],
|
||||
'/beat/detail': ['action'], // it nests too, in this case, because of the above line, this is /beat/:beatId/detail/:action
|
||||
});
|
||||
```
|
||||
|
||||
In the above example to allow for flexability, the `./pages/beat.tsx` page would receve a prop of `routes` that is an array of sub-pages
|
|
@ -8,7 +8,6 @@ import { flatten, get as _get, omit } from 'lodash';
|
|||
import { INDEX_NAMES } from '../../../../common/constants';
|
||||
import { CMBeat } from '../../../../common/domain_types';
|
||||
import { DatabaseAdapter } from '../database/adapter_types';
|
||||
|
||||
import { FrameworkUser } from '../framework/adapter_types';
|
||||
import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types';
|
||||
|
||||
|
@ -71,7 +70,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
|
|||
const ids = beatIds.map(beatId => `beat:${beatId}`);
|
||||
|
||||
const params = {
|
||||
_sourceInclude: ['beat.id', 'beat.verified_on'],
|
||||
body: {
|
||||
ids,
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { FrameworkRequest, FrameworkUser } from '../framework/adapter_types';
|
||||
|
||||
export interface DatabaseAdapter {
|
||||
putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise<any>;
|
||||
get<Source>(
|
||||
|
@ -63,7 +64,7 @@ export interface DatabaseSearchParams extends DatabaseGenericParams {
|
|||
sort?: DatabaseNameList;
|
||||
_source?: DatabaseNameList;
|
||||
_sourceExclude?: DatabaseNameList;
|
||||
_sourceInclude?: DatabaseNameList;
|
||||
_source_includes?: DatabaseNameList;
|
||||
terminateAfter?: number;
|
||||
stats?: DatabaseNameList;
|
||||
suggestField?: string;
|
||||
|
@ -142,7 +143,7 @@ export interface DatabaseBulkIndexDocumentsParams extends DatabaseGenericParams
|
|||
fields?: DatabaseNameList;
|
||||
_source?: DatabaseNameList;
|
||||
_sourceExclude?: DatabaseNameList;
|
||||
_sourceInclude?: DatabaseNameList;
|
||||
_source_includes?: DatabaseNameList;
|
||||
pipeline?: string;
|
||||
index?: string;
|
||||
}
|
||||
|
@ -154,7 +155,7 @@ export interface DatabaseMGetParams extends DatabaseGenericParams {
|
|||
refresh?: boolean;
|
||||
_source?: DatabaseNameList;
|
||||
_sourceExclude?: DatabaseNameList;
|
||||
_sourceInclude?: DatabaseNameList;
|
||||
_source_includes?: DatabaseNameList;
|
||||
index: string;
|
||||
type?: string;
|
||||
}
|
||||
|
@ -273,7 +274,7 @@ export interface DatabaseGetParams extends DatabaseGenericParams {
|
|||
routing?: string;
|
||||
_source?: DatabaseNameList;
|
||||
_sourceExclude?: DatabaseNameList;
|
||||
_sourceInclude?: DatabaseNameList;
|
||||
_source_includes?: DatabaseNameList;
|
||||
version?: number;
|
||||
versionType?: DatabaseVersionType;
|
||||
id: string;
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { internalAuthData } from '../../../utils/wrap_request';
|
||||
import { FrameworkUser } from '../framework/adapter_types';
|
||||
import { internalAuthData } from './../framework/adapter_types';
|
||||
import {
|
||||
DatabaseAdapter,
|
||||
DatabaseBulkIndexDocumentsParams,
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
|
||||
// @ts-ignore
|
||||
import { createEsTestCluster } from '@kbn/test';
|
||||
import { config as beatsPluginConfig, configPrefix } from '../../../../..';
|
||||
import { config as beatsPluginConfig } from '../../../../..';
|
||||
// @ts-ignore
|
||||
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
|
||||
import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter';
|
||||
import { PLUGIN } from './../../../../../common/constants/plugin';
|
||||
import { CONFIG_PREFIX } from './../../../../../common/constants/plugin';
|
||||
import { contractTests } from './test_contract';
|
||||
|
||||
const kbnServer = kbnTestServer.createRootWithCorePlugins({ server: { maxPayloadBytes: 100 } });
|
||||
|
@ -21,7 +23,7 @@ contractTests('Kibana Framework Adapter', {
|
|||
await kbnServer.start();
|
||||
|
||||
const config = legacyServer.server.config();
|
||||
config.extendSchema(beatsPluginConfig, {}, configPrefix);
|
||||
config.extendSchema(beatsPluginConfig, {}, CONFIG_PREFIX);
|
||||
|
||||
config.set('xpack.beats.encryptionKey', 'foo');
|
||||
},
|
||||
|
@ -29,6 +31,6 @@ contractTests('Kibana Framework Adapter', {
|
|||
await kbnServer.shutdown();
|
||||
},
|
||||
adapterSetup: () => {
|
||||
return new KibanaBackendFrameworkAdapter(legacyServer.server);
|
||||
return new KibanaBackendFrameworkAdapter(PLUGIN.ID, legacyServer.server);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,21 +4,118 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Lifecycle, ResponseToolkit } from 'hapi';
|
||||
import { internalAuthData } from '../../../utils/wrap_request';
|
||||
import * as t from 'io-ts';
|
||||
import { LicenseType } from '../../../../common/constants/security';
|
||||
|
||||
export const internalAuthData = Symbol('internalAuthData');
|
||||
export const internalUser: FrameworkInternalUser = {
|
||||
kind: 'internal',
|
||||
};
|
||||
|
||||
export interface XpackInfo {
|
||||
license: {
|
||||
getType: () => LicenseType;
|
||||
/** Is the license expired */
|
||||
isActive: () => boolean;
|
||||
getExpiryDateInMillis: () => number;
|
||||
};
|
||||
feature: (pluginId: string) => any;
|
||||
isAvailable: () => boolean;
|
||||
}
|
||||
|
||||
export interface BackendFrameworkAdapter {
|
||||
internalUser: FrameworkInternalUser;
|
||||
version: string;
|
||||
info: null | FrameworkInfo;
|
||||
log(text: string): void;
|
||||
on(event: 'xpack.status.green', cb: () => void): void;
|
||||
getSetting(settingPath: string): any;
|
||||
exposeStaticDir(urlPath: string, dir: string): void;
|
||||
registerRoute<
|
||||
RouteRequest extends FrameworkWrappableRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
>(
|
||||
registerRoute<RouteRequest extends FrameworkRequest, RouteResponse extends FrameworkResponse>(
|
||||
route: FrameworkRouteOptions<RouteRequest, RouteResponse>
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface KibanaLegacyServer {
|
||||
plugins: {
|
||||
xpack_main: {
|
||||
status: {
|
||||
once: (status: 'green' | 'yellow' | 'red', callback: () => void) => void;
|
||||
};
|
||||
info: XpackInfo;
|
||||
};
|
||||
kibana: {
|
||||
status: {
|
||||
plugin: {
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
security: {
|
||||
getUser: (request: KibanaServerRequest) => any;
|
||||
};
|
||||
elasticsearch: {
|
||||
getCluster: () => any;
|
||||
};
|
||||
beats_management: {};
|
||||
};
|
||||
config: () => any;
|
||||
route: (routeConfig: any) => void;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
export const RuntimeFrameworkInfo = t.interface(
|
||||
{
|
||||
kibana: t.type({
|
||||
version: t.string,
|
||||
}),
|
||||
license: t.type({
|
||||
type: t.union(
|
||||
['oss', 'trial', 'standard', 'basic', 'gold', 'platinum'].map(s => t.literal(s))
|
||||
),
|
||||
expired: t.boolean,
|
||||
expiry_date_in_millis: t.number,
|
||||
}),
|
||||
security: t.type({
|
||||
enabled: t.boolean,
|
||||
available: t.boolean,
|
||||
}),
|
||||
watcher: t.type({
|
||||
enabled: t.boolean,
|
||||
available: t.boolean,
|
||||
}),
|
||||
},
|
||||
'FrameworkInfo'
|
||||
);
|
||||
export interface FrameworkInfo extends t.TypeOf<typeof RuntimeFrameworkInfo> {}
|
||||
|
||||
export const RuntimeKibanaServerRequest = t.interface(
|
||||
{
|
||||
params: t.object,
|
||||
payload: t.object,
|
||||
query: t.object,
|
||||
headers: t.type({
|
||||
authorization: t.union([t.string, t.null]),
|
||||
}),
|
||||
info: t.type({
|
||||
remoteAddress: t.string,
|
||||
}),
|
||||
},
|
||||
'KibanaServerRequest'
|
||||
);
|
||||
export interface KibanaServerRequest extends t.TypeOf<typeof RuntimeKibanaServerRequest> {}
|
||||
|
||||
export const RuntimeKibanaUser = t.interface(
|
||||
{
|
||||
username: t.string,
|
||||
roles: t.array(t.string),
|
||||
full_name: t.union([t.null, t.string]),
|
||||
email: t.union([t.null, t.string]),
|
||||
enabled: t.boolean,
|
||||
},
|
||||
'KibanaUser'
|
||||
);
|
||||
export interface KibanaUser extends t.TypeOf<typeof RuntimeKibanaUser> {}
|
||||
|
||||
export interface FrameworkAuthenticatedUser<AuthDataType = any> {
|
||||
kind: 'authenticated';
|
||||
[internalAuthData]: AuthDataType;
|
||||
|
@ -26,9 +123,6 @@ export interface FrameworkAuthenticatedUser<AuthDataType = any> {
|
|||
roles: string[];
|
||||
full_name: string | null;
|
||||
email: string | null;
|
||||
metadata: {
|
||||
[key: string]: any;
|
||||
};
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -45,46 +139,32 @@ export type FrameworkUser<AuthDataType = any> =
|
|||
| FrameworkUnAuthenticatedUser
|
||||
| FrameworkInternalUser;
|
||||
export interface FrameworkRequest<
|
||||
InternalRequest extends FrameworkWrappableRequest = FrameworkWrappableRequest
|
||||
KibanaServerRequestGenaric extends Partial<KibanaServerRequest> = any
|
||||
> {
|
||||
user: FrameworkUser<InternalRequest['headers']>;
|
||||
headers: InternalRequest['headers'];
|
||||
info: InternalRequest['info'];
|
||||
payload: InternalRequest['payload'];
|
||||
params: InternalRequest['params'];
|
||||
query: InternalRequest['query'];
|
||||
user: FrameworkUser<KibanaServerRequestGenaric['headers']>;
|
||||
headers: KibanaServerRequestGenaric['headers'];
|
||||
info: KibanaServerRequest['info'];
|
||||
payload: KibanaServerRequestGenaric['payload'];
|
||||
params: KibanaServerRequestGenaric['params'];
|
||||
query: KibanaServerRequestGenaric['query'];
|
||||
}
|
||||
|
||||
export interface FrameworkRouteOptions<
|
||||
RouteRequest extends FrameworkWrappableRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
RouteRequest extends FrameworkRequest = FrameworkRequest,
|
||||
RouteResponse extends FrameworkResponse = any
|
||||
> {
|
||||
path: string;
|
||||
method: string | string[];
|
||||
vhost?: string;
|
||||
licenseRequired?: boolean;
|
||||
licenseRequired?: string[];
|
||||
requiredRoles?: string[];
|
||||
handler: FrameworkRouteHandler<RouteRequest, RouteResponse>;
|
||||
config?: {};
|
||||
}
|
||||
|
||||
export type FrameworkRouteHandler<
|
||||
RouteRequest extends FrameworkWrappableRequest,
|
||||
RouteRequest extends KibanaServerRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
> = (request: FrameworkRequest<RouteRequest>, h: ResponseToolkit) => void;
|
||||
|
||||
export interface FrameworkWrappableRequest<
|
||||
Payload = any,
|
||||
Params = any,
|
||||
Query = any,
|
||||
Headers = any,
|
||||
Info = any
|
||||
> {
|
||||
headers: Headers;
|
||||
info: Info;
|
||||
payload: Payload;
|
||||
params: Params;
|
||||
query: Query;
|
||||
}
|
||||
|
||||
export type FrameworkResponse = Lifecycle.ReturnValue;
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { wrapRequest } from '../../../utils/wrap_request';
|
||||
import { FrameworkInternalUser } from './adapter_types';
|
||||
import { LicenseType } from './../../../../common/constants/security';
|
||||
import { KibanaServerRequest } from './adapter_types';
|
||||
import {
|
||||
BackendFrameworkAdapter,
|
||||
FrameworkInfo,
|
||||
FrameworkRequest,
|
||||
FrameworkResponse,
|
||||
FrameworkRouteOptions,
|
||||
FrameworkWrappableRequest,
|
||||
internalAuthData,
|
||||
internalUser,
|
||||
} from './adapter_types';
|
||||
|
||||
interface TestSettings {
|
||||
|
@ -19,10 +22,9 @@ interface TestSettings {
|
|||
}
|
||||
|
||||
export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
||||
public readonly internalUser: FrameworkInternalUser = {
|
||||
kind: 'internal',
|
||||
};
|
||||
public version: string;
|
||||
public info: null | FrameworkInfo = null;
|
||||
public readonly internalUser = internalUser;
|
||||
|
||||
private settings: TestSettings;
|
||||
private server: any;
|
||||
|
||||
|
@ -31,13 +33,40 @@ export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
|||
encryptionKey: 'something_who_cares',
|
||||
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
|
||||
},
|
||||
hapiServer?: any
|
||||
hapiServer?: any,
|
||||
license: LicenseType = 'trial',
|
||||
securityEnabled: boolean = true,
|
||||
licenseActive: boolean = true
|
||||
) {
|
||||
this.server = hapiServer;
|
||||
this.settings = settings;
|
||||
this.version = 'testing';
|
||||
}
|
||||
const now = new Date();
|
||||
|
||||
this.info = {
|
||||
kibana: {
|
||||
version: 'unknown',
|
||||
},
|
||||
license: {
|
||||
type: license,
|
||||
expired: !licenseActive,
|
||||
expiry_date_in_millis: new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime(),
|
||||
},
|
||||
security: {
|
||||
enabled: securityEnabled,
|
||||
available: securityEnabled,
|
||||
},
|
||||
watcher: {
|
||||
enabled: true,
|
||||
available: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
public log(text: string) {
|
||||
this.server.log(text);
|
||||
}
|
||||
public on(event: 'xpack.status.green', cb: () => void) {
|
||||
cb();
|
||||
}
|
||||
public getSetting(settingPath: string) {
|
||||
switch (settingPath) {
|
||||
case 'xpack.beats.enrollmentTokensTtlInSeconds':
|
||||
|
@ -63,18 +92,18 @@ export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
|||
}
|
||||
|
||||
public registerRoute<
|
||||
RouteRequest extends FrameworkWrappableRequest,
|
||||
RouteRequest extends FrameworkRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
>(route: FrameworkRouteOptions<RouteRequest, RouteResponse>) {
|
||||
if (!this.server) {
|
||||
throw new Error('Must pass a hapi server into the adapter to use registerRoute');
|
||||
}
|
||||
const wrappedHandler = (licenseRequired: boolean) => (request: any, h: any) => {
|
||||
return route.handler(wrapRequest(request), h);
|
||||
const wrappedHandler = (licenseRequired: string[]) => (request: any, h: any) => {
|
||||
return route.handler(this.wrapRequest(request), h);
|
||||
};
|
||||
|
||||
this.server.route({
|
||||
handler: wrappedHandler(route.licenseRequired || false),
|
||||
handler: wrappedHandler(route.licenseRequired || []),
|
||||
method: route.method,
|
||||
path: route.path,
|
||||
config: {
|
||||
|
@ -87,4 +116,33 @@ export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
|||
public async injectRequstForTesting({ method, url, headers, payload }: any) {
|
||||
return await this.server.inject({ method, url, headers, payload });
|
||||
}
|
||||
|
||||
private wrapRequest<InternalRequest extends KibanaServerRequest>(
|
||||
req: InternalRequest
|
||||
): FrameworkRequest<InternalRequest> {
|
||||
const { params, payload, query, headers, info } = req;
|
||||
|
||||
const isAuthenticated = headers.authorization != null;
|
||||
|
||||
return {
|
||||
user: isAuthenticated
|
||||
? {
|
||||
kind: 'authenticated',
|
||||
[internalAuthData]: headers,
|
||||
username: 'elastic',
|
||||
roles: ['superuser'],
|
||||
full_name: null,
|
||||
email: null,
|
||||
enabled: true,
|
||||
}
|
||||
: {
|
||||
kind: 'unauthenticated',
|
||||
},
|
||||
headers,
|
||||
info,
|
||||
params,
|
||||
payload,
|
||||
query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,61 +4,64 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import Boom from 'boom';
|
||||
import { difference } from 'lodash';
|
||||
import { ResponseToolkit } from 'hapi';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import { get } from 'lodash';
|
||||
// @ts-ignore
|
||||
import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status';
|
||||
import { PLUGIN } from '../../../../common/constants/plugin';
|
||||
import { wrapRequest } from '../../../utils/wrap_request';
|
||||
import { FrameworkRequest } from './adapter_types';
|
||||
import {
|
||||
BackendFrameworkAdapter,
|
||||
FrameworkInternalUser,
|
||||
FrameworkInfo,
|
||||
FrameworkRequest,
|
||||
FrameworkResponse,
|
||||
FrameworkRouteOptions,
|
||||
FrameworkWrappableRequest,
|
||||
internalAuthData,
|
||||
internalUser,
|
||||
KibanaLegacyServer,
|
||||
KibanaServerRequest,
|
||||
KibanaUser,
|
||||
RuntimeFrameworkInfo,
|
||||
RuntimeKibanaUser,
|
||||
XpackInfo,
|
||||
} from './adapter_types';
|
||||
|
||||
export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
||||
public readonly internalUser: FrameworkInternalUser = {
|
||||
kind: 'internal',
|
||||
};
|
||||
public version: string;
|
||||
private server: any;
|
||||
private cryptoHash: string | null;
|
||||
public readonly internalUser = internalUser;
|
||||
public info: null | FrameworkInfo = null;
|
||||
|
||||
constructor(hapiServer: any) {
|
||||
this.server = hapiServer;
|
||||
if (hapiServer.plugins.kibana) {
|
||||
this.version = hapiServer.plugins.kibana.status.plugin.version;
|
||||
} else {
|
||||
this.version = 'unknown';
|
||||
}
|
||||
this.cryptoHash = null;
|
||||
this.validateConfig();
|
||||
|
||||
const xpackMainPlugin = hapiServer.plugins.xpack_main;
|
||||
const thisPlugin = hapiServer.plugins.beats_management;
|
||||
constructor(
|
||||
private readonly PLUGIN_ID: string,
|
||||
private readonly server: KibanaLegacyServer,
|
||||
private readonly CONFIG_PREFIX?: string
|
||||
) {
|
||||
const xpackMainPlugin = this.server.plugins.xpack_main;
|
||||
const thisPlugin = this.server.plugins.beats_management;
|
||||
|
||||
mirrorPluginStatus(xpackMainPlugin, thisPlugin);
|
||||
|
||||
xpackMainPlugin.status.once('green', () => {
|
||||
this.xpackInfoWasUpdatedHandler(xpackMainPlugin.info);
|
||||
// Register a function that is called whenever the xpack info changes,
|
||||
// to re-compute the license check results for this plugin
|
||||
xpackMainPlugin.info
|
||||
.feature(PLUGIN.ID)
|
||||
.registerLicenseCheckResultsGenerator((xPackInfo: any) => this.checkLicense(xPackInfo));
|
||||
.feature(this.PLUGIN_ID)
|
||||
.registerLicenseCheckResultsGenerator(this.xpackInfoWasUpdatedHandler);
|
||||
});
|
||||
}
|
||||
// TODO make base path a constructor level param
|
||||
public getSetting(settingPath: string) {
|
||||
// TODO type check server properly
|
||||
if (settingPath === 'xpack.beats.encryptionKey') {
|
||||
// @ts-ignore
|
||||
return this.server.config().get(settingPath) || this.cryptoHash;
|
||||
|
||||
public on(event: 'xpack.status.green', cb: () => void) {
|
||||
switch (event) {
|
||||
case 'xpack.status.green':
|
||||
this.server.plugins.xpack_main.status.once('green', cb);
|
||||
}
|
||||
// @ts-ignore
|
||||
return this.server.config().get(settingPath) || this.cryptoHash;
|
||||
}
|
||||
|
||||
public getSetting(settingPath: string) {
|
||||
return this.server.config().get(settingPath);
|
||||
}
|
||||
|
||||
public log(text: string) {
|
||||
this.server.log(text);
|
||||
}
|
||||
|
||||
public exposeStaticDir(urlPath: string, dir: string): void {
|
||||
|
@ -74,136 +77,120 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter {
|
|||
}
|
||||
|
||||
public registerRoute<
|
||||
RouteRequest extends FrameworkWrappableRequest,
|
||||
RouteRequest extends FrameworkRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
>(route: FrameworkRouteOptions<RouteRequest, RouteResponse>) {
|
||||
const hasAny = (roles: string[], requiredRoles: string[]) =>
|
||||
requiredRoles.some(r => roles.includes(r));
|
||||
|
||||
const wrappedHandler = (licenseRequired: boolean, requiredRoles?: string[]) => async (
|
||||
request: any,
|
||||
h: any
|
||||
) => {
|
||||
const xpackMainPlugin = this.server.plugins.xpack_main;
|
||||
const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults();
|
||||
if (licenseRequired && !licenseCheckResults.licenseValid) {
|
||||
return Boom.forbidden(licenseCheckResults.message);
|
||||
}
|
||||
const wrappedRequest = wrapRequest(request);
|
||||
if (requiredRoles) {
|
||||
if (wrappedRequest.user.kind !== 'authenticated') {
|
||||
return h.response().code(403);
|
||||
}
|
||||
wrappedRequest.user = {
|
||||
...wrappedRequest.user,
|
||||
...(await this.getUser(request)),
|
||||
};
|
||||
|
||||
if (
|
||||
wrappedRequest.user.kind === 'authenticated' &&
|
||||
(!hasAny(wrappedRequest.user.roles, this.getSetting('xpack.beats.defaultUserRoles')) ||
|
||||
!wrappedRequest.user.roles) &&
|
||||
difference(requiredRoles, wrappedRequest.user.roles).length !== 0
|
||||
) {
|
||||
return h.response().code(403);
|
||||
}
|
||||
}
|
||||
return route.handler(wrappedRequest, h);
|
||||
};
|
||||
|
||||
this.server.route({
|
||||
handler: wrappedHandler(route.licenseRequired || false, route.requiredRoles),
|
||||
handler: async (request: KibanaServerRequest, h: ResponseToolkit) => {
|
||||
// Note, RuntimeKibanaServerRequest is avalaible to validate request, and its type *is* KibanaServerRequest
|
||||
// but is not used here for perf reasons. It's value here is not high enough...
|
||||
return await route.handler(await this.wrapRequest<RouteRequest>(request), h);
|
||||
},
|
||||
method: route.method,
|
||||
path: route.path,
|
||||
config: route.config,
|
||||
});
|
||||
}
|
||||
|
||||
private async getUser(request: FrameworkRequest) {
|
||||
private async wrapRequest<InternalRequest extends KibanaServerRequest>(
|
||||
req: KibanaServerRequest
|
||||
): Promise<FrameworkRequest<InternalRequest>> {
|
||||
const { params, payload, query, headers, info } = req;
|
||||
|
||||
let isAuthenticated = headers.authorization != null;
|
||||
let user;
|
||||
if (isAuthenticated) {
|
||||
user = await this.getUser(req);
|
||||
if (!user) {
|
||||
isAuthenticated = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
user:
|
||||
isAuthenticated && user
|
||||
? {
|
||||
kind: 'authenticated',
|
||||
[internalAuthData]: headers,
|
||||
...user,
|
||||
}
|
||||
: {
|
||||
kind: 'unauthenticated',
|
||||
},
|
||||
headers,
|
||||
info,
|
||||
params,
|
||||
payload,
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
private async getUser(request: KibanaServerRequest): Promise<KibanaUser | null> {
|
||||
let user;
|
||||
try {
|
||||
return await this.server.plugins.security.getUser(request);
|
||||
user = await this.server.plugins.security.getUser(request);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make key a param
|
||||
private validateConfig() {
|
||||
// @ts-ignore
|
||||
const config = this.server.config();
|
||||
const encryptionKey = config.get('xpack.beats.encryptionKey');
|
||||
|
||||
if (!encryptionKey) {
|
||||
this.server.log(
|
||||
'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token'
|
||||
const assertKibanaUser = RuntimeKibanaUser.decode(user);
|
||||
if (assertKibanaUser.isLeft()) {
|
||||
throw new Error(
|
||||
`Error parsing user info in ${this.PLUGIN_ID}, ${
|
||||
PathReporter.report(assertKibanaUser)[0]
|
||||
}`
|
||||
);
|
||||
this.cryptoHash = 'xpack_beats_default_encryptionKey';
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// TODO this should NOT be in an adapter, break up and move validation to a lib
|
||||
private checkLicense(xPackInfo: any) {
|
||||
private xpackInfoWasUpdatedHandler = (xpackInfo: XpackInfo) => {
|
||||
let xpackInfoUnpacked: FrameworkInfo;
|
||||
|
||||
// If, for some reason, we cannot get the license information
|
||||
// from Elasticsearch, assume worst case and disable the Logstash pipeline UI
|
||||
if (!xPackInfo || !xPackInfo.isAvailable()) {
|
||||
return {
|
||||
securityEnabled: false,
|
||||
licenseValid: false,
|
||||
message:
|
||||
'You cannot manage Beats central management because license information is not available at this time.',
|
||||
};
|
||||
// from Elasticsearch, assume worst case and disable
|
||||
if (!xpackInfo || !xpackInfo.isAvailable()) {
|
||||
this.info = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const VALID_LICENSE_MODES = ['trial', 'standard', 'gold', 'platinum'];
|
||||
|
||||
const isLicenseValid = xPackInfo.license.isOneOf(VALID_LICENSE_MODES);
|
||||
const isLicenseActive = xPackInfo.license.isActive();
|
||||
const licenseType = xPackInfo.license.getType();
|
||||
const isSecurityEnabled = xPackInfo.feature('security').isEnabled();
|
||||
|
||||
// License is not valid
|
||||
if (!isLicenseValid) {
|
||||
return {
|
||||
defaultUserRoles: this.getSetting('xpack.beats.defaultUserRoles'),
|
||||
securityEnabled: true,
|
||||
licenseValid: false,
|
||||
licenseExpired: false,
|
||||
message: `Your ${licenseType} license does not support Beats central management features. Please upgrade your license.`,
|
||||
try {
|
||||
xpackInfoUnpacked = {
|
||||
kibana: {
|
||||
version: get(this.server, 'plugins.kibana.status.plugin.version', 'unknown'),
|
||||
},
|
||||
license: {
|
||||
type: xpackInfo.license.getType(),
|
||||
expired: !xpackInfo.license.isActive(),
|
||||
expiry_date_in_millis:
|
||||
xpackInfo.license.getExpiryDateInMillis() !== undefined
|
||||
? xpackInfo.license.getExpiryDateInMillis()
|
||||
: -1,
|
||||
},
|
||||
security: {
|
||||
enabled: !!xpackInfo.feature('security') && xpackInfo.feature('security').isEnabled(),
|
||||
available: !!xpackInfo.feature('security'),
|
||||
},
|
||||
watcher: {
|
||||
enabled: !!xpackInfo.feature('watcher') && xpackInfo.feature('watcher').isEnabled(),
|
||||
available: !!xpackInfo.feature('watcher'),
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
this.server.log(`Error accessing required xPackInfo in ${this.PLUGIN_ID} Kibana adapter`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// License is valid but not active, we go into a read-only mode.
|
||||
if (!isLicenseActive) {
|
||||
return {
|
||||
defaultUserRoles: this.getSetting('xpack.beats.defaultUserRoles'),
|
||||
securityEnabled: true,
|
||||
licenseValid: true,
|
||||
licenseExpired: true,
|
||||
message: `You cannot edit, create, or delete your Beats central management configurations because your ${licenseType} license has expired.`,
|
||||
};
|
||||
const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked);
|
||||
if (assertData.isLeft()) {
|
||||
throw new Error(
|
||||
`Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}`
|
||||
);
|
||||
}
|
||||
this.info = xpackInfoUnpacked;
|
||||
|
||||
// Security is not enabled in ES
|
||||
if (!isSecurityEnabled) {
|
||||
const message =
|
||||
'Security must be enabled in order to use Beats central management features.' +
|
||||
' Please set xpack.security.enabled: true in your elasticsearch.yml.';
|
||||
return {
|
||||
defaultUserRoles: this.getSetting('xpack.beats.defaultUserRoles'),
|
||||
securityEnabled: false,
|
||||
licenseValid: true,
|
||||
licenseExpired: false,
|
||||
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
// License is valid and active
|
||||
return {
|
||||
defaultUserRoles: this.getSetting('xpack.beats.defaultUserRoles'),
|
||||
securityEnabled: true,
|
||||
licenseValid: true,
|
||||
licenseExpired: false,
|
||||
security: xpackInfoUnpacked.security,
|
||||
settings: this.getSetting(this.CONFIG_PREFIX || this.PLUGIN_ID),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter {
|
|||
|
||||
public async getAll(user: FrameworkUser, ESQuery?: any) {
|
||||
const params = {
|
||||
ignore: [404],
|
||||
_source: true,
|
||||
size: 10000,
|
||||
index: INDEX_NAMES.BEATS,
|
||||
|
@ -112,6 +113,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter {
|
|||
const ids = tagIds.map(tag => `tag:${tag}`);
|
||||
|
||||
const params = {
|
||||
ignore: [404],
|
||||
_source: true,
|
||||
body: {
|
||||
ids,
|
||||
|
|
|
@ -11,7 +11,7 @@ export interface TokenEnrollmentData {
|
|||
}
|
||||
|
||||
export interface CMTokensAdapter {
|
||||
deleteEnrollmentToken(enrollmentToken: string): Promise<void>;
|
||||
getEnrollmentToken(enrollmentToken: string): Promise<TokenEnrollmentData>;
|
||||
deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise<void>;
|
||||
getEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise<TokenEnrollmentData>;
|
||||
upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise<TokenEnrollmentData[]>;
|
||||
}
|
||||
|
|
|
@ -7,29 +7,26 @@
|
|||
import { flatten, get } from 'lodash';
|
||||
import { INDEX_NAMES } from '../../../../common/constants';
|
||||
import { DatabaseAdapter } from '../database/adapter_types';
|
||||
import { BackendFrameworkAdapter, FrameworkUser } from '../framework/adapter_types';
|
||||
import { FrameworkUser } from '../framework/adapter_types';
|
||||
import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types';
|
||||
|
||||
export class ElasticsearchTokensAdapter implements CMTokensAdapter {
|
||||
private database: DatabaseAdapter;
|
||||
private framework: BackendFrameworkAdapter;
|
||||
constructor(private readonly database: DatabaseAdapter) {}
|
||||
|
||||
constructor(database: DatabaseAdapter, framework: BackendFrameworkAdapter) {
|
||||
this.database = database;
|
||||
this.framework = framework;
|
||||
}
|
||||
|
||||
public async deleteEnrollmentToken(enrollmentToken: string) {
|
||||
public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) {
|
||||
const params = {
|
||||
id: `enrollment_token:${enrollmentToken}`,
|
||||
index: INDEX_NAMES.BEATS,
|
||||
type: '_doc',
|
||||
};
|
||||
|
||||
await this.database.delete(this.framework.internalUser, params);
|
||||
await this.database.delete(user, params);
|
||||
}
|
||||
|
||||
public async getEnrollmentToken(tokenString: string): Promise<TokenEnrollmentData> {
|
||||
public async getEnrollmentToken(
|
||||
user: FrameworkUser,
|
||||
tokenString: string
|
||||
): Promise<TokenEnrollmentData> {
|
||||
const params = {
|
||||
id: `enrollment_token:${tokenString}`,
|
||||
ignore: [404],
|
||||
|
@ -37,7 +34,7 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter {
|
|||
type: '_doc',
|
||||
};
|
||||
|
||||
const response = await this.database.get(this.framework.internalUser, params);
|
||||
const response = await this.database.get(user, params);
|
||||
const tokenDetails = get<TokenEnrollmentData>(response, '_source.enrollment_token', {
|
||||
expires_on: '0',
|
||||
token: null,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FrameworkAuthenticatedUser } from '../framework/adapter_types';
|
||||
import { FrameworkAuthenticatedUser, FrameworkUser } from '../framework/adapter_types';
|
||||
import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types';
|
||||
|
||||
export class MemoryTokensAdapter implements CMTokensAdapter {
|
||||
|
@ -14,7 +14,7 @@ export class MemoryTokensAdapter implements CMTokensAdapter {
|
|||
this.tokenDB = tokenDB;
|
||||
}
|
||||
|
||||
public async deleteEnrollmentToken(enrollmentToken: string) {
|
||||
public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) {
|
||||
const index = this.tokenDB.findIndex(token => token.token === enrollmentToken);
|
||||
|
||||
if (index > -1) {
|
||||
|
@ -22,7 +22,10 @@ export class MemoryTokensAdapter implements CMTokensAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
public async getEnrollmentToken(tokenString: string): Promise<TokenEnrollmentData> {
|
||||
public async getEnrollmentToken(
|
||||
user: FrameworkUser,
|
||||
tokenString: string
|
||||
): Promise<TokenEnrollmentData> {
|
||||
return new Promise<TokenEnrollmentData>(resolve => {
|
||||
return resolve(this.tokenDB.find(token => token.token === tokenString));
|
||||
});
|
||||
|
|
|
@ -6,26 +6,26 @@
|
|||
|
||||
import { uniq } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { findNonExistentItems } from '../../utils/find_non_existent_items';
|
||||
import { findNonExistentItems } from '../utils/find_non_existent_items';
|
||||
|
||||
import { CMBeat } from '../../../common/domain_types';
|
||||
import { BeatsTagAssignment, CMBeatsAdapter } from '../adapters/beats/adapter_types';
|
||||
import { FrameworkUser } from '../adapters/framework/adapter_types';
|
||||
import { CMBeat } from '../../common/domain_types';
|
||||
import { BeatsTagAssignment, CMBeatsAdapter } from './adapters/beats/adapter_types';
|
||||
import { FrameworkUser } from './adapters/framework/adapter_types';
|
||||
|
||||
import { CMAssignmentReturn } from '../adapters/beats/adapter_types';
|
||||
import { BeatsRemovalReturn } from '../adapters/beats/adapter_types';
|
||||
import { BeatEnrollmentStatus, CMDomainLibs, CMServerLibs, UserOrToken } from '../lib';
|
||||
import { CMAssignmentReturn } from './adapters/beats/adapter_types';
|
||||
import { BeatsRemovalReturn } from './adapters/beats/adapter_types';
|
||||
import { BeatEnrollmentStatus, CMServerLibs, UserOrToken } from './types';
|
||||
|
||||
export class CMBeatsDomain {
|
||||
private tags: CMDomainLibs['tags'];
|
||||
private tokens: CMDomainLibs['tokens'];
|
||||
private tags: CMServerLibs['tags'];
|
||||
private tokens: CMServerLibs['tokens'];
|
||||
private framework: CMServerLibs['framework'];
|
||||
|
||||
constructor(
|
||||
private readonly adapter: CMBeatsAdapter,
|
||||
libs: {
|
||||
tags: CMDomainLibs['tags'];
|
||||
tokens: CMDomainLibs['tokens'];
|
||||
tags: CMServerLibs['tags'];
|
||||
tokens: CMServerLibs['tokens'];
|
||||
framework: CMServerLibs['framework'];
|
||||
}
|
||||
) {
|
||||
|
@ -60,7 +60,7 @@ export class CMBeatsDomain {
|
|||
public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial<CMBeat>) {
|
||||
const beat = await this.adapter.get(this.framework.internalUser, beatId);
|
||||
|
||||
// TODO make return type enum
|
||||
// FIXME make return type enum
|
||||
if (beat === null) {
|
||||
return 'beat-not-found';
|
||||
}
|
||||
|
@ -83,7 +83,6 @@ export class CMBeatsDomain {
|
|||
});
|
||||
}
|
||||
|
||||
// TODO more strongly type this
|
||||
public async enrollBeat(
|
||||
enrollmentToken: string,
|
||||
beatId: string,
|
||||
|
@ -148,7 +147,7 @@ export class CMBeatsDomain {
|
|||
'removals'
|
||||
);
|
||||
|
||||
// TODO abstract this
|
||||
// FIXME abstract this
|
||||
const validRemovals = removals
|
||||
.map((removal, idxInRequest) => ({
|
||||
beatId: removal.beatId,
|
||||
|
@ -180,8 +179,8 @@ export class CMBeatsDomain {
|
|||
const nonExistentBeatIds = findNonExistentItems(beats, beatIds);
|
||||
const nonExistentTags = findNonExistentItems(tags, tagIds);
|
||||
|
||||
// TODO break out back into route / function response
|
||||
// TODO causes function to error if a beat or tag does not exist
|
||||
// FIXME break out back into route / function response
|
||||
// FIXME causes function to error if a beat or tag does not exist
|
||||
addNonExistentItemToResponse(
|
||||
response,
|
||||
assignments,
|
||||
|
@ -190,7 +189,7 @@ export class CMBeatsDomain {
|
|||
'assignments'
|
||||
);
|
||||
|
||||
// TODO abstract this
|
||||
// FIXME abstract this
|
||||
const validAssignments = assignments
|
||||
.map((assignment, idxInRequest) => ({
|
||||
beatId: assignment.beatId,
|
||||
|
@ -209,7 +208,7 @@ export class CMBeatsDomain {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO abstract to the route, also the key arg is a temp fix
|
||||
// FIXME abstract to the route, also the key arg is a temp fix
|
||||
function addNonExistentItemToResponse(
|
||||
response: any,
|
||||
assignments: any,
|
|
@ -10,19 +10,26 @@ import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_ad
|
|||
import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter';
|
||||
|
||||
import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
|
||||
import { BackendFrameworkLib } from './../framework';
|
||||
|
||||
import { CMBeatsDomain } from '../domains/beats';
|
||||
import { CMTagsDomain } from '../domains/tags';
|
||||
import { CMTokensDomain } from '../domains/tokens';
|
||||
import { CMBeatsDomain } from '../beats';
|
||||
import { CMTagsDomain } from '../tags';
|
||||
import { CMTokensDomain } from '../tokens';
|
||||
|
||||
import { CMDomainLibs, CMServerLibs } from '../lib';
|
||||
import { PLUGIN } from 'x-pack/plugins/beats_management/common/constants';
|
||||
import { CONFIG_PREFIX } from 'x-pack/plugins/beats_management/common/constants/plugin';
|
||||
import { DatabaseKbnESPlugin } from '../adapters/database/adapter_types';
|
||||
import { KibanaLegacyServer } from '../adapters/framework/adapter_types';
|
||||
import { CMServerLibs } from '../types';
|
||||
|
||||
export function compose(server: any): CMServerLibs {
|
||||
const framework = new KibanaBackendFrameworkAdapter(server);
|
||||
const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch);
|
||||
export function compose(server: KibanaLegacyServer): CMServerLibs {
|
||||
const framework = new BackendFrameworkLib(
|
||||
new KibanaBackendFrameworkAdapter(PLUGIN.ID, server, CONFIG_PREFIX)
|
||||
);
|
||||
const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch as DatabaseKbnESPlugin);
|
||||
|
||||
const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(database));
|
||||
const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database, framework), {
|
||||
const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database), {
|
||||
framework,
|
||||
});
|
||||
const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(database), {
|
||||
|
@ -31,17 +38,13 @@ export function compose(server: any): CMServerLibs {
|
|||
framework,
|
||||
});
|
||||
|
||||
const domainLibs: CMDomainLibs = {
|
||||
const libs: CMServerLibs = {
|
||||
framework,
|
||||
database,
|
||||
beats,
|
||||
tags,
|
||||
tokens,
|
||||
};
|
||||
|
||||
const libs: CMServerLibs = {
|
||||
framework,
|
||||
database,
|
||||
...domainLibs,
|
||||
};
|
||||
|
||||
return libs;
|
||||
}
|
||||
|
|
|
@ -10,14 +10,15 @@ import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter';
|
|||
|
||||
import { HapiBackendFrameworkAdapter } from '../adapters/framework/hapi_framework_adapter';
|
||||
|
||||
import { CMBeatsDomain } from '../domains/beats';
|
||||
import { CMTagsDomain } from '../domains/tags';
|
||||
import { CMTokensDomain } from '../domains/tokens';
|
||||
import { CMBeatsDomain } from '../beats';
|
||||
import { CMTagsDomain } from '../tags';
|
||||
import { CMTokensDomain } from '../tokens';
|
||||
|
||||
import { CMDomainLibs, CMServerLibs } from '../lib';
|
||||
import { BackendFrameworkLib } from '../framework';
|
||||
import { CMServerLibs } from '../types';
|
||||
|
||||
export function compose(server: any): CMServerLibs {
|
||||
const framework = new HapiBackendFrameworkAdapter(undefined, server);
|
||||
const framework = new BackendFrameworkLib(new HapiBackendFrameworkAdapter(undefined, server));
|
||||
|
||||
const tags = new CMTagsDomain(new MemoryTagsAdapter(server.tagsDB || []));
|
||||
const tokens = new CMTokensDomain(new MemoryTokensAdapter(server.tokensDB || []), {
|
||||
|
@ -29,16 +30,12 @@ export function compose(server: any): CMServerLibs {
|
|||
framework,
|
||||
});
|
||||
|
||||
const domainLibs: CMDomainLibs = {
|
||||
const libs: CMServerLibs = {
|
||||
framework,
|
||||
beats,
|
||||
tags,
|
||||
tokens,
|
||||
};
|
||||
|
||||
const libs: CMServerLibs = {
|
||||
framework,
|
||||
...domainLibs,
|
||||
};
|
||||
|
||||
return libs;
|
||||
}
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FrameworkInternalUser } from '../../../adapters/framework/adapter_types';
|
||||
|
||||
import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter';
|
||||
import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter';
|
||||
import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter';
|
||||
import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter';
|
||||
|
||||
import { BeatTag, CMBeat } from '../../../../../common/domain_types';
|
||||
|
||||
import { CMBeatsDomain } from '../../beats';
|
||||
import { CMTagsDomain } from '../../tags';
|
||||
import { CMTokensDomain } from '../../tokens';
|
||||
|
||||
import Chance from 'chance';
|
||||
|
||||
const seed = Date.now();
|
||||
const chance = new Chance(seed);
|
||||
|
||||
const internalUser: FrameworkInternalUser = { kind: 'internal' };
|
||||
|
||||
const settings = {
|
||||
encryptionKey: 'something_who_cares',
|
||||
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
|
||||
};
|
||||
|
||||
describe('Beats Domain Lib', () => {
|
||||
let beatsLib: CMBeatsDomain;
|
||||
let beatsDB: CMBeat[] = [];
|
||||
let tagsDB: BeatTag[] = [];
|
||||
|
||||
describe('assign_tags_to_beats', () => {
|
||||
beforeEach(async () => {
|
||||
beatsDB = [
|
||||
{
|
||||
access_token: '9a6c99ae0fd84b068819701169cd8a4b',
|
||||
config_status: 'OK',
|
||||
active: true,
|
||||
enrollment_token: '23423423423',
|
||||
host_ip: '1.2.3.4',
|
||||
host_name: 'foo.bar.com',
|
||||
id: 'qux',
|
||||
type: 'filebeat',
|
||||
},
|
||||
{
|
||||
access_token: '188255eb560a4448b72656c5e99cae6f',
|
||||
active: true,
|
||||
config_status: 'OK',
|
||||
enrollment_token: 'reertrte',
|
||||
host_ip: '22.33.11.44',
|
||||
host_name: 'baz.bar.com',
|
||||
id: 'baz',
|
||||
type: 'metricbeat',
|
||||
},
|
||||
{
|
||||
access_token: '93c4a4dd08564c189a7ec4e4f046b975',
|
||||
active: true,
|
||||
enrollment_token: '23s423423423',
|
||||
config_status: 'OK',
|
||||
host_ip: '1.2.3.4',
|
||||
host_name: 'foo.bar.com',
|
||||
id: 'foo',
|
||||
tags: ['production', 'qa'],
|
||||
type: 'metricbeat',
|
||||
verified_on: '2018-05-15T16:25:38.924Z',
|
||||
},
|
||||
{
|
||||
access_token: '3c4a4dd08564c189a7ec4e4f046b9759',
|
||||
enrollment_token: 'gdfsgdf',
|
||||
active: true,
|
||||
config_status: 'OK',
|
||||
host_ip: '11.22.33.44',
|
||||
host_name: 'foo.com',
|
||||
id: 'bar',
|
||||
type: 'filebeat',
|
||||
},
|
||||
];
|
||||
tagsDB = [
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'production',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'development',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'qa',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
];
|
||||
const framework = new HapiBackendFrameworkAdapter(settings);
|
||||
|
||||
const tokensLib = new CMTokensDomain(new MemoryTokensAdapter([]), {
|
||||
framework,
|
||||
});
|
||||
|
||||
const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB));
|
||||
|
||||
beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), {
|
||||
tags: tagsLib,
|
||||
tokens: tokensLib,
|
||||
framework,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a single tag to a single beat', async () => {
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'bar', tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]);
|
||||
});
|
||||
|
||||
it('should not re-add an existing tag to a beat', async () => {
|
||||
const tags = ['production'];
|
||||
|
||||
let beat = beatsDB.find(b => b.id === 'foo') as any;
|
||||
expect(beat.tags).toEqual([...tags, 'qa']);
|
||||
|
||||
// Adding the existing tag
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'foo', tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]);
|
||||
|
||||
beat = beatsDB.find(b => b.id === 'foo') as any;
|
||||
expect(beat.tags).toEqual([...tags, 'qa']);
|
||||
});
|
||||
|
||||
it('should add a single tag to a multiple beats', async () => {
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'foo', tag: 'development' },
|
||||
{ beatId: 'bar', tag: 'development' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{ status: 200, result: 'updated' },
|
||||
{ status: 200, result: 'updated' },
|
||||
]);
|
||||
|
||||
let beat = beatsDB.find(b => b.id === 'foo') as any;
|
||||
expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it
|
||||
|
||||
beat = beatsDB.find(b => b.id === 'bar') as any;
|
||||
expect(beat.tags).toEqual(['development']);
|
||||
});
|
||||
|
||||
it('should add multiple tags to a single beat', async () => {
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'bar', tag: 'development' },
|
||||
{ beatId: 'bar', tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{ status: 200, result: 'updated' },
|
||||
{ status: 200, result: 'updated' },
|
||||
]);
|
||||
|
||||
const beat = beatsDB.find(b => b.id === 'bar') as any;
|
||||
expect(beat.tags).toEqual(['development', 'production']);
|
||||
});
|
||||
|
||||
it('should add multiple tags to a multiple beats', async () => {
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'foo', tag: 'development' },
|
||||
{ beatId: 'bar', tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{ status: 200, result: 'updated' },
|
||||
{ status: 200, result: 'updated' },
|
||||
]);
|
||||
|
||||
let beat = beatsDB.find(b => b.id === 'foo') as any;
|
||||
expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it
|
||||
|
||||
beat = beatsDB.find(b => b.id === 'bar') as any;
|
||||
expect(beat.tags).toEqual(['production']);
|
||||
});
|
||||
|
||||
it('should return errors for non-existent beats', async () => {
|
||||
const nonExistentBeatId = chance.word();
|
||||
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: nonExistentBeatId, tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{ status: 404, result: `Beat ${nonExistentBeatId} not found` },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return errors for non-existent tags', async () => {
|
||||
const nonExistentTag = chance.word();
|
||||
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: 'bar', tag: nonExistentTag },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{ status: 404, result: `Tag ${nonExistentTag} not found` },
|
||||
]);
|
||||
|
||||
const beat = beatsDB.find(b => b.id === 'bar') as any;
|
||||
expect(beat).not.toHaveProperty('tags');
|
||||
});
|
||||
|
||||
it('should return errors for non-existent beats and tags', async () => {
|
||||
const nonExistentBeatId = chance.word();
|
||||
const nonExistentTag = chance.word();
|
||||
|
||||
const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [
|
||||
{ beatId: nonExistentBeatId, tag: nonExistentTag },
|
||||
]);
|
||||
|
||||
expect(apiResponse.assignments).toEqual([
|
||||
{
|
||||
result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`,
|
||||
status: 404,
|
||||
},
|
||||
]);
|
||||
|
||||
const beat = beatsDB.find(b => b.id === 'bar') as any;
|
||||
expect(beat).not.toHaveProperty('tags');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter';
|
||||
import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter';
|
||||
import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter';
|
||||
import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter';
|
||||
import { BeatEnrollmentStatus } from '../../../lib';
|
||||
|
||||
import { BeatTag, CMBeat } from '../../../../../common/domain_types';
|
||||
import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types';
|
||||
|
||||
import { CMBeatsDomain } from '../../beats';
|
||||
import { CMTagsDomain } from '../../tags';
|
||||
import { CMTokensDomain } from '../../tokens';
|
||||
|
||||
import Chance from 'chance';
|
||||
import { sign as signToken } from 'jsonwebtoken';
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
const seed = Date.now();
|
||||
const chance = new Chance(seed);
|
||||
|
||||
const settings = {
|
||||
encryptionKey: 'something_who_cares',
|
||||
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
|
||||
};
|
||||
|
||||
describe('Beats Domain Lib', () => {
|
||||
let beatsLib: CMBeatsDomain;
|
||||
let tokensLib: CMTokensDomain;
|
||||
|
||||
let beatsDB: CMBeat[] = [];
|
||||
let tagsDB: BeatTag[] = [];
|
||||
let tokensDB: TokenEnrollmentData[] = [];
|
||||
let validEnrollmentToken: string;
|
||||
let beatId: string;
|
||||
let beat: Partial<CMBeat>;
|
||||
|
||||
describe('enroll_beat', () => {
|
||||
beforeEach(async () => {
|
||||
validEnrollmentToken = chance.word();
|
||||
beatId = chance.word();
|
||||
|
||||
beatsDB = [];
|
||||
tagsDB = [];
|
||||
tokensDB = [
|
||||
{
|
||||
expires_on: moment()
|
||||
.add(4, 'hours')
|
||||
.toJSON(),
|
||||
token: validEnrollmentToken,
|
||||
},
|
||||
];
|
||||
|
||||
const version =
|
||||
chance.integer({ min: 1, max: 10 }) +
|
||||
'.' +
|
||||
chance.integer({ min: 1, max: 10 }) +
|
||||
'.' +
|
||||
chance.integer({ min: 1, max: 10 });
|
||||
|
||||
beat = {
|
||||
host_name: 'foo.bar.com',
|
||||
type: 'filebeat',
|
||||
version,
|
||||
};
|
||||
|
||||
const framework = new HapiBackendFrameworkAdapter(settings);
|
||||
|
||||
tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), {
|
||||
framework,
|
||||
});
|
||||
|
||||
const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB));
|
||||
|
||||
beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), {
|
||||
tags: tagsLib,
|
||||
tokens: tokensLib,
|
||||
framework,
|
||||
});
|
||||
});
|
||||
|
||||
it('should enroll beat, returning an access token', async () => {
|
||||
const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken);
|
||||
|
||||
expect(token).toEqual(validEnrollmentToken);
|
||||
const { accessToken, status } = await beatsLib.enrollBeat(
|
||||
validEnrollmentToken,
|
||||
beatId,
|
||||
'192.168.1.1',
|
||||
omit(beat, 'enrollment_token')
|
||||
);
|
||||
expect(status).toEqual(BeatEnrollmentStatus.Success);
|
||||
|
||||
expect(beatsDB.length).toEqual(1);
|
||||
expect(beatsDB[0]).toHaveProperty('host_ip');
|
||||
expect(beatsDB[0]).toHaveProperty('verified_on');
|
||||
|
||||
expect(accessToken).toEqual(beatsDB[0].access_token);
|
||||
|
||||
await tokensLib.deleteEnrollmentToken(validEnrollmentToken);
|
||||
|
||||
expect(tokensDB.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should reject an invalid enrollment token', async () => {
|
||||
const { token } = await tokensLib.getEnrollmentToken(chance.word());
|
||||
|
||||
expect(token).toEqual(null);
|
||||
});
|
||||
|
||||
it('should reject an expired enrollment token', async () => {
|
||||
const { token } = await tokensLib.getEnrollmentToken(
|
||||
signToken({}, settings.encryptionKey, {
|
||||
expiresIn: '-1min',
|
||||
})
|
||||
);
|
||||
|
||||
expect(token).toEqual(null);
|
||||
});
|
||||
|
||||
it('should delete the given enrollment token so it may not be reused', async () => {
|
||||
expect(tokensDB[0].token).toEqual(validEnrollmentToken);
|
||||
await tokensLib.deleteEnrollmentToken(validEnrollmentToken);
|
||||
expect(tokensDB.length).toEqual(0);
|
||||
|
||||
const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken);
|
||||
|
||||
expect(token).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { BeatTag, CMBeat } from '../../../../../common/domain_types';
|
||||
import { FrameworkInternalUser } from '../../../adapters/framework/adapter_types';
|
||||
import { compose } from '../../../compose/testing';
|
||||
import { CMServerLibs } from '../../../lib';
|
||||
|
||||
const internalUser: FrameworkInternalUser = { kind: 'internal' };
|
||||
|
||||
describe('Beats Domain Lib', () => {
|
||||
let libs: CMServerLibs;
|
||||
let beatsDB: Array<Partial<CMBeat>> = [];
|
||||
let tagsDB: BeatTag[] = [];
|
||||
|
||||
describe('remove_tags_from_beats', () => {
|
||||
beforeEach(async () => {
|
||||
beatsDB = [
|
||||
{
|
||||
access_token: '9a6c99ae0fd84b068819701169cd8a4b',
|
||||
active: true,
|
||||
enrollment_token: '123kuil;4',
|
||||
host_ip: '1.2.3.4',
|
||||
host_name: 'foo.bar.com',
|
||||
id: 'qux',
|
||||
type: 'filebeat',
|
||||
},
|
||||
{
|
||||
access_token: '188255eb560a4448b72656c5e99cae6f',
|
||||
active: true,
|
||||
enrollment_token: '12fghjyu34',
|
||||
host_ip: '22.33.11.44',
|
||||
host_name: 'baz.bar.com',
|
||||
id: 'baz',
|
||||
type: 'metricbeat',
|
||||
},
|
||||
{
|
||||
access_token: '93c4a4dd08564c189a7ec4e4f046b975',
|
||||
active: true,
|
||||
enrollment_token: '12nfhgj34',
|
||||
host_ip: '1.2.3.4',
|
||||
host_name: 'foo.bar.com',
|
||||
id: 'foo',
|
||||
tags: ['production', 'qa'],
|
||||
type: 'metricbeat',
|
||||
verified_on: '2018-05-15T16:25:38.924Z',
|
||||
},
|
||||
{
|
||||
access_token: '3c4a4dd08564c189a7ec4e4f046b9759',
|
||||
active: true,
|
||||
|
||||
enrollment_token: '123sfd4',
|
||||
host_ip: '11.22.33.44',
|
||||
host_name: 'foo.com',
|
||||
id: 'bar',
|
||||
type: 'filebeat',
|
||||
},
|
||||
];
|
||||
tagsDB = [
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'production',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'development',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
{
|
||||
configuration_blocks: [],
|
||||
id: 'qa',
|
||||
last_updated: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
libs = compose({
|
||||
tagsDB,
|
||||
beatsDB,
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove a single tag from a single beat', async () => {
|
||||
const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [
|
||||
{ beatId: 'foo', tag: 'production' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.removals).toEqual([{ status: 200, result: 'updated' }]);
|
||||
// @ts-ignore
|
||||
expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['qa']);
|
||||
});
|
||||
|
||||
it('should remove a single tag from a multiple beats', async () => {
|
||||
const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [
|
||||
{ beatId: 'foo', tag: 'development' },
|
||||
{ beatId: 'bar', tag: 'development' },
|
||||
]);
|
||||
|
||||
expect(apiResponse.removals).toEqual([
|
||||
{ status: 200, result: 'updated' },
|
||||
{ status: 200, result: 'updated' },
|
||||
]);
|
||||
|
||||
// @ts-ignore
|
||||
expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['production', 'qa']);
|
||||
expect(beatsDB.find(b => b.id === 'bar')).not.toHaveProperty('tags');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Chance from 'chance';
|
||||
import { BeatTag, CMBeat } from '../../../../../common/domain_types';
|
||||
import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter';
|
||||
import { HapiBackendFrameworkAdapter } from '../../../adapters/framework/hapi_framework_adapter';
|
||||
import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter';
|
||||
import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types';
|
||||
import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter';
|
||||
import { CMBeatsDomain } from '../../beats';
|
||||
import { CMTagsDomain } from '../../tags';
|
||||
import { CMTokensDomain } from '../../tokens';
|
||||
|
||||
const seed = Date.now();
|
||||
const chance = new Chance(seed);
|
||||
|
||||
const settings = {
|
||||
encryptionKey: `it's_a_secret`,
|
||||
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
|
||||
};
|
||||
|
||||
describe('Beats Domain lib', () => {
|
||||
describe('update_beat', () => {
|
||||
let beatsLib: CMBeatsDomain;
|
||||
let tokensLib: CMTokensDomain;
|
||||
let token: TokenEnrollmentData;
|
||||
let beatsDB: CMBeat[] = [];
|
||||
let tagsDB: BeatTag[] = [];
|
||||
let tokensDB: TokenEnrollmentData[];
|
||||
let beatId: string;
|
||||
let beat: Partial<CMBeat>;
|
||||
|
||||
const getBeatsLib = async () => {
|
||||
const framework = new HapiBackendFrameworkAdapter(settings);
|
||||
|
||||
tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { framework });
|
||||
const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB));
|
||||
|
||||
beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), {
|
||||
framework,
|
||||
tags: tagsLib,
|
||||
tokens: tokensLib,
|
||||
});
|
||||
|
||||
await tokensLib.createEnrollmentTokens(framework.internalUser, 1);
|
||||
token = tokensDB[0];
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
beatId = chance.word();
|
||||
beat = {
|
||||
host_name: 'foo.bar.com',
|
||||
type: 'filebeat',
|
||||
version: '6.4.0',
|
||||
};
|
||||
beatsDB = [];
|
||||
tagsDB = [];
|
||||
tokensDB = [];
|
||||
|
||||
getBeatsLib();
|
||||
});
|
||||
|
||||
it('should return a not-found message if beat does not exist', async () => {
|
||||
const tokenString = token.token || '';
|
||||
const result = await beatsLib.update(tokenString, beatId, beat);
|
||||
|
||||
expect(result).toBe('beat-not-found');
|
||||
});
|
||||
|
||||
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 || '',
|
||||
type: 'filebeat',
|
||||
host_ip: 'localhost',
|
||||
host_name: 'foo.bar.com',
|
||||
};
|
||||
beatsDB = [beatToFind];
|
||||
|
||||
getBeatsLib();
|
||||
|
||||
const result = await beatsLib.update('something_invalid', beatId, beat);
|
||||
|
||||
expect(result).toBe('invalid-access-token');
|
||||
});
|
||||
|
||||
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 || '',
|
||||
type: 'metricbeat',
|
||||
host_ip: 'localhost',
|
||||
host_name: 'bar.foo.com',
|
||||
version: '6.3.5',
|
||||
};
|
||||
beatsDB = [beatToFind];
|
||||
getBeatsLib();
|
||||
// @ts-ignore
|
||||
await beatsLib.update(token, beatId, beat);
|
||||
expect(beatsDB).toHaveLength(1);
|
||||
const updatedBeat = beatsDB[0];
|
||||
expect(updatedBeat.id).toBe(beatId);
|
||||
expect(updatedBeat.host_name).toBe('foo.bar.com');
|
||||
expect(updatedBeat.version).toBe('6.4.0');
|
||||
expect(updatedBeat.type).toBe('filebeat');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { HapiBackendFrameworkAdapter } from '../../adapters/framework/hapi_framework_adapter';
|
||||
import { TokenEnrollmentData } from '../../adapters/tokens/adapter_types';
|
||||
import { MemoryTokensAdapter } from '../../adapters/tokens/memory_tokens_adapter';
|
||||
import { CMTokensDomain } from '../tokens';
|
||||
|
||||
import Chance from 'chance';
|
||||
import moment from 'moment';
|
||||
import { BackendFrameworkAdapter } from '../../adapters/framework/adapter_types';
|
||||
|
||||
const seed = Date.now();
|
||||
const chance = new Chance(seed);
|
||||
|
||||
const settings = {
|
||||
encryptionKey: 'something_who_cares',
|
||||
enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes
|
||||
};
|
||||
|
||||
describe('Token Domain Lib', () => {
|
||||
let tokensLib: CMTokensDomain;
|
||||
let tokensDB: TokenEnrollmentData[] = [];
|
||||
let framework: BackendFrameworkAdapter;
|
||||
|
||||
beforeEach(async () => {
|
||||
tokensDB = [];
|
||||
framework = new HapiBackendFrameworkAdapter(settings);
|
||||
|
||||
tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), {
|
||||
framework,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate webtokens with a qty of 1', async () => {
|
||||
const tokens = await tokensLib.createEnrollmentTokens(framework.internalUser, 1);
|
||||
|
||||
expect(tokens.length).toBe(1);
|
||||
|
||||
expect(typeof tokens[0]).toBe('string');
|
||||
});
|
||||
|
||||
it('should create the specified number of tokens', async () => {
|
||||
const numTokens = chance.integer({ min: 1, max: 20 });
|
||||
const tokensFromApi = await tokensLib.createEnrollmentTokens(framework.internalUser, numTokens);
|
||||
|
||||
expect(tokensFromApi.length).toEqual(numTokens);
|
||||
expect(tokensFromApi).toEqual(tokensDB.map((t: TokenEnrollmentData) => t.token));
|
||||
});
|
||||
|
||||
it('should set token expiration to 10 minutes from now by default', async () => {
|
||||
await tokensLib.createEnrollmentTokens(framework.internalUser, 1);
|
||||
|
||||
const token = tokensDB[0];
|
||||
|
||||
// We do a fuzzy check to see if the token expires between 9 and 10 minutes
|
||||
// from now because a bit of time has elapsed been the creation of the
|
||||
// tokens and this check.
|
||||
const tokenExpiresOn = moment(token.expires_on).valueOf();
|
||||
|
||||
// Because sometimes the test runs so fast it it equal, and we dont use expect.js version that has toBeLessThanOrEqualTo
|
||||
const tenMinutesFromNow = moment()
|
||||
.add('10', 'minutes')
|
||||
.add('1', 'seconds')
|
||||
.valueOf();
|
||||
|
||||
const almostTenMinutesFromNow = moment(tenMinutesFromNow)
|
||||
.subtract('2', 'seconds')
|
||||
.valueOf();
|
||||
expect(tokenExpiresOn).toBeLessThan(tenMinutesFromNow);
|
||||
expect(tokenExpiresOn).toBeGreaterThan(almostTenMinutesFromNow);
|
||||
});
|
||||
});
|
104
x-pack/plugins/beats_management/server/lib/framework.ts
Normal file
104
x-pack/plugins/beats_management/server/lib/framework.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { difference } from 'lodash';
|
||||
import { FrameworkRouteHandler } from './adapters/framework/adapter_types';
|
||||
import { FrameworkRequest } from './adapters/framework/adapter_types';
|
||||
import {
|
||||
BackendFrameworkAdapter,
|
||||
FrameworkResponse,
|
||||
FrameworkRouteOptions,
|
||||
} from './adapters/framework/adapter_types';
|
||||
|
||||
export class BackendFrameworkLib {
|
||||
public exposeStaticDir = this.adapter.exposeStaticDir;
|
||||
public internalUser = this.adapter.internalUser;
|
||||
constructor(private readonly adapter: BackendFrameworkAdapter) {
|
||||
this.validateConfig();
|
||||
}
|
||||
|
||||
public registerRoute<
|
||||
RouteRequest extends FrameworkRequest,
|
||||
RouteResponse extends FrameworkResponse
|
||||
>(route: FrameworkRouteOptions<RouteRequest, RouteResponse>) {
|
||||
this.adapter.registerRoute({
|
||||
...route,
|
||||
handler: this.wrapRouteWithSecurity(
|
||||
route.handler,
|
||||
route.licenseRequired || [],
|
||||
route.requiredRoles
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
public getSetting(setting: 'encryptionKey'): string;
|
||||
public getSetting(setting: 'enrollmentTokensTtlInSeconds'): number;
|
||||
public getSetting(setting: 'defaultUserRoles'): string[];
|
||||
public getSetting(
|
||||
setting: 'encryptionKey' | 'enrollmentTokensTtlInSeconds' | 'defaultUserRoles'
|
||||
) {
|
||||
return this.adapter.getSetting(`xpack.beats.${setting}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expired `null` happens when we have no xpack info
|
||||
*/
|
||||
get license() {
|
||||
return {
|
||||
type: this.adapter.info ? this.adapter.info.license.type : 'unknown',
|
||||
expired: this.adapter.info ? this.adapter.info.license.expired : null,
|
||||
};
|
||||
}
|
||||
|
||||
get securityIsEnabled() {
|
||||
return this.adapter.info ? this.adapter.info.security.enabled : false;
|
||||
}
|
||||
|
||||
private validateConfig() {
|
||||
const encryptionKey = this.adapter.getSetting('xpack.beats.encryptionKey');
|
||||
|
||||
if (!encryptionKey) {
|
||||
this.adapter.log(
|
||||
'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private wrapRouteWithSecurity(
|
||||
handler: FrameworkRouteHandler<any, any>,
|
||||
requiredLicense: string[],
|
||||
requiredRoles?: string[]
|
||||
) {
|
||||
return async (request: FrameworkRequest, h: any) => {
|
||||
if (
|
||||
requiredLicense.length > 0 &&
|
||||
(this.license.expired || !requiredLicense.includes(this.license.type))
|
||||
) {
|
||||
return Boom.forbidden(
|
||||
`Your ${
|
||||
this.license
|
||||
} license does not support this API or is expired. Please upgrade your license.`
|
||||
);
|
||||
}
|
||||
|
||||
if (requiredRoles) {
|
||||
if (request.user.kind !== 'authenticated') {
|
||||
return h.response().code(403);
|
||||
}
|
||||
|
||||
if (
|
||||
request.user.kind === 'authenticated' &&
|
||||
!request.user.roles.includes('superuser') &&
|
||||
difference(requiredRoles, request.user.roles).length !== 0
|
||||
) {
|
||||
return h.response().code(403);
|
||||
}
|
||||
}
|
||||
return await handler(request, h);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -5,12 +5,12 @@
|
|||
*/
|
||||
|
||||
import { intersection, uniq, values } from 'lodash';
|
||||
import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants';
|
||||
import { ConfigurationBlock } from '../../../common/domain_types';
|
||||
import { FrameworkUser } from '../adapters/framework/adapter_types';
|
||||
import { UNIQUENESS_ENFORCING_TYPES } from '../../common/constants';
|
||||
import { ConfigurationBlock } from '../../common/domain_types';
|
||||
import { FrameworkUser } from './adapters/framework/adapter_types';
|
||||
|
||||
import { entries } from '../../utils/polyfills';
|
||||
import { CMTagsAdapter } from '../adapters/tags/adapter_types';
|
||||
import { entries } from '../utils/polyfills';
|
||||
import { CMTagsAdapter } from './adapters/tags/adapter_types';
|
||||
|
||||
export class CMTagsDomain {
|
||||
constructor(private readonly adapter: CMTagsAdapter) {}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue