From 80d9fe5bb782bad6c46ff1c9b10e2c54864b8a01 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 11 Dec 2018 17:59:07 -0500 Subject: [PATCH] [Beats CM] Backport refactor(#26636) to 6.x (#26886) * [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 759a14561dbeb52fba70515384ee35873e43a409. * 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 * Update x-pack/plugins/beats_management/public/utils/page_loader.test.ts Co-Authored-By: mattapperson * Update x-pack/plugins/beats_management/public/utils/page_loader.ts Co-Authored-By: mattapperson * 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 --- x-pack/package.json | 4 +- .../common/constants/index.ts | 3 +- .../common/constants/plugin.ts | 1 + .../common/constants/security.ts | 10 + x-pack/plugins/beats_management/index.ts | 9 +- .../enroll_beats.tsx} | 131 ++------ .../layouts/background.tsx} | 13 +- .../public/components/layouts/header.tsx | 36 --- .../public/components/layouts/no_data.tsx | 18 +- .../public/components/layouts/primary.tsx | 112 ++++--- .../public/components/layouts/walkthrough.tsx | 1 - .../public/components/loading.tsx | 16 + .../navigation/breadcrumb/breadcrumb.tsx | 62 ++++ .../breadcrumb}/consumer.tsx | 0 .../breadcrumb}/context.tsx | 0 .../breadcrumb}/index.ts | 2 +- .../breadcrumb}/provider.tsx | 0 .../breadcrumb}/types.d.ts | 0 .../components/navigation/child_routes.tsx | 38 +++ .../{ => navigation}/connected_link.tsx | 3 +- .../route_with_breadcrumb.tsx | 88 ----- .../public/components/table/controls.tsx | 2 +- .../controls}/action_control.tsx | 0 .../controls}/index.ts | 0 .../controls}/option_control.tsx | 2 +- .../controls}/tag_assignment.tsx | 4 +- .../controls}/tag_badge_list.tsx | 2 +- .../components/table/table_type_configs.tsx | 5 +- .../tag/config_view/config_form.tsx | 2 +- .../components/tag/config_view/index.tsx | 2 - .../public/components/tag/tag_edit.tsx | 38 +-- .../beats_management/public/config_schemas.ts | 2 +- .../public/config_schemas_translations_map.ts | 7 +- .../public/containers/beats.ts | 98 ++++++ .../public/containers/tags.ts | 45 +++ .../containers/with_kuery_autocompletion.tsx | 2 +- .../public/containers/with_url_state.tsx | 6 +- .../public/frontend_types.d.ts | 34 ++ .../plugins/beats_management/public/index.tsx | 68 ++-- .../lib/adapters/beats/rest_beats_adapter.ts | 2 +- .../public/lib/adapters/elasticsearch/rest.ts | 1 - .../lib/adapters/framework/adapter_types.ts | 81 +++++ .../framework/kibana_framework_adapter.ts | 290 ++++++++++------- .../lib/adapters/rest_api/adapter_types.ts | 2 +- .../rest_api/axios_rest_api_adapter.ts | 2 +- .../public/lib/adapters/tags/adapter_types.ts | 2 +- .../lib/adapters/tags/memory_tags_adapter.ts | 2 +- .../lib/adapters/tags/rest_tags_adapter.ts | 4 +- .../beats_management/public/lib/beats.ts | 46 ++- .../public/lib/compose/kibana.ts | 44 ++- .../public/lib/compose/memory.ts | 34 +- .../beats_management/public/lib/framework.ts | 40 +++ .../beats_management/public/lib/tags.ts | 6 +- .../public/lib/{lib.ts => types.ts} | 31 +- .../public/pages/{404.tsx => __404.tsx} | 0 .../public/pages/beat/action_section.tsx | 75 ----- .../public/pages/beat/activity.tsx | 22 -- .../pages/beat/{detail.tsx => details.tsx} | 25 +- .../public/pages/beat/index.tsx | 206 ++++++------ .../public/pages/beat/tags.tsx | 34 +- .../pages/{ => error}/enforce_security.tsx | 2 +- .../pages/{ => error}/invalid_license.tsx | 2 +- .../public/pages/{ => error}/no_access.tsx | 2 +- .../public/pages/main/activity.tsx | 21 -- .../public/pages/main/index.tsx | 304 ------------------ .../public/pages/main/tags.tsx | 126 -------- .../pages/overview/configuration_tags.tsx | 126 ++++++++ .../beats.tsx => overview/enrolled_beats.tsx} | 276 +++++++--------- .../public/pages/overview/index.tsx | 91 ++++++ .../public/pages/{tag/index.tsx => tag.tsx} | 62 ++-- .../public/pages/walkthrough/initial/beat.tsx | 63 ++++ .../initial/finish.tsx} | 46 ++- .../pages/walkthrough/initial/index.tsx | 86 +++++ .../initial/tag.tsx} | 56 +--- .../beats_management/public/router.tsx | 222 +++++++------ .../__snapshots__/page_loader.test.ts.snap | 143 ++++++++ .../utils/page_loader/page_loader.test.ts | 138 ++++++++ .../public/utils/page_loader/page_loader.ts | 170 ++++++++++ .../public/utils/page_loader/readme.md | 21 ++ .../beats/elasticsearch_beats_adapter.ts | 2 - .../lib/adapters/database/adapter_types.ts | 9 +- .../database/kibana_database_adapter.ts | 2 +- .../adapters/framework/__tests__/kibana.ts | 8 +- .../lib/adapters/framework/adapter_types.ts | 148 +++++++-- .../framework/hapi_framework_adapter.ts | 86 ++++- .../framework/kibana_framework_adapter.ts | 269 ++++++++-------- .../tags/elasticsearch_tags_adapter.ts | 2 + .../lib/adapters/tokens/adapter_types.ts | 4 +- .../tokens/elasticsearch_tokens_adapter.ts | 21 +- .../adapters/tokens/memory_tokens_adapter.ts | 9 +- .../server/lib/{domains => }/beats.ts | 35 +- .../server/lib/compose/kibana.ts | 33 +- .../server/lib/compose/testing.ts | 19 +- .../__tests__/beats/assign_tags.test.ts | 237 -------------- .../domains/__tests__/beats/enroll.test.ts | 137 -------- .../__tests__/beats/remove_tags.test.ts | 112 ------- .../domains/__tests__/beats/update.test.ts | 118 ------- .../lib/domains/__tests__/tokens.test.ts | 76 ----- .../beats_management/server/lib/framework.ts | 104 ++++++ .../server/lib/{domains => }/tags.ts | 10 +- .../server/lib/{domains => }/tokens.ts | 25 +- .../server/lib/{lib.ts => types.ts} | 18 +- .../server/management_server.ts | 2 +- .../__tests__/beats_assignments.test.ts | 26 +- .../server/rest_api/__tests__/test_harnes.ts | 2 +- .../server/rest_api/beats/configuration.ts | 2 +- .../server/rest_api/beats/enroll.ts | 10 +- .../server/rest_api/beats/get.ts | 2 +- .../server/rest_api/beats/list.ts | 8 +- .../server/rest_api/beats/tag_assignment.ts | 7 +- .../server/rest_api/beats/tag_removal.ts | 7 +- .../server/rest_api/beats/update.ts | 7 +- .../server/rest_api/tags/delete.ts | 5 +- .../server/rest_api/tags/get.ts | 8 +- .../server/rest_api/tags/list.ts | 5 +- .../server/rest_api/tags/set.ts | 6 +- .../server/rest_api/tokens/create.ts | 5 +- .../server/utils/wrap_request.ts | 7 +- x-pack/plugins/beats_management/wallaby.js | 8 +- x-pack/test/functional/config.js | 37 +-- yarn.lock | 24 ++ 121 files changed, 2804 insertions(+), 2630 deletions(-) create mode 100644 x-pack/plugins/beats_management/common/constants/security.ts rename x-pack/plugins/beats_management/public/{pages/main/enroll_fragment.tsx => components/enroll_beats.tsx} (69%) rename x-pack/plugins/beats_management/public/{app.d.ts => components/layouts/background.tsx} (56%) delete mode 100644 x-pack/plugins/beats_management/public/components/layouts/header.tsx create mode 100644 x-pack/plugins/beats_management/public/components/loading.tsx create mode 100644 x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx rename x-pack/plugins/beats_management/public/components/{route_with_breadcrumb => navigation/breadcrumb}/consumer.tsx (100%) rename x-pack/plugins/beats_management/public/components/{route_with_breadcrumb => navigation/breadcrumb}/context.tsx (100%) rename x-pack/plugins/beats_management/public/components/{route_with_breadcrumb => navigation/breadcrumb}/index.ts (84%) rename x-pack/plugins/beats_management/public/components/{route_with_breadcrumb => navigation/breadcrumb}/provider.tsx (100%) rename x-pack/plugins/beats_management/public/components/{route_with_breadcrumb => navigation/breadcrumb}/types.d.ts (100%) create mode 100644 x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx rename x-pack/plugins/beats_management/public/components/{ => navigation}/connected_link.tsx (96%) delete mode 100644 x-pack/plugins/beats_management/public/components/route_with_breadcrumb/route_with_breadcrumb.tsx rename x-pack/plugins/beats_management/public/components/{table_controls => table/controls}/action_control.tsx (100%) rename x-pack/plugins/beats_management/public/components/{table_controls => table/controls}/index.ts (100%) rename x-pack/plugins/beats_management/public/components/{table_controls => table/controls}/option_control.tsx (99%) rename x-pack/plugins/beats_management/public/components/{table_controls => table/controls}/tag_assignment.tsx (92%) rename x-pack/plugins/beats_management/public/components/{table_controls => table/controls}/tag_badge_list.tsx (94%) create mode 100644 x-pack/plugins/beats_management/public/containers/beats.ts create mode 100644 x-pack/plugins/beats_management/public/containers/tags.ts create mode 100644 x-pack/plugins/beats_management/public/frontend_types.d.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/framework.ts rename x-pack/plugins/beats_management/public/lib/{lib.ts => types.ts} (73%) rename x-pack/plugins/beats_management/public/pages/{404.tsx => __404.tsx} (100%) delete mode 100644 x-pack/plugins/beats_management/public/pages/beat/action_section.tsx delete mode 100644 x-pack/plugins/beats_management/public/pages/beat/activity.tsx rename x-pack/plugins/beats_management/public/pages/beat/{detail.tsx => details.tsx} (88%) rename x-pack/plugins/beats_management/public/pages/{ => error}/enforce_security.tsx (92%) rename x-pack/plugins/beats_management/public/pages/{ => error}/invalid_license.tsx (93%) rename x-pack/plugins/beats_management/public/pages/{ => error}/no_access.tsx (93%) delete mode 100644 x-pack/plugins/beats_management/public/pages/main/activity.tsx delete mode 100644 x-pack/plugins/beats_management/public/pages/main/index.tsx delete mode 100644 x-pack/plugins/beats_management/public/pages/main/tags.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx rename x-pack/plugins/beats_management/public/pages/{main/beats.tsx => overview/enrolled_beats.tsx} (57%) create mode 100644 x-pack/plugins/beats_management/public/pages/overview/index.tsx rename x-pack/plugins/beats_management/public/pages/{tag/index.tsx => tag.tsx} (78%) create mode 100644 x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx rename x-pack/plugins/beats_management/public/pages/{main/finish_walkthrough.tsx => walkthrough/initial/finish.tsx} (74%) create mode 100644 x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx rename x-pack/plugins/beats_management/public/pages/{main/create_tag_fragment.tsx => walkthrough/initial/tag.tsx} (55%) create mode 100644 x-pack/plugins/beats_management/public/utils/page_loader/__snapshots__/page_loader.test.ts.snap create mode 100644 x-pack/plugins/beats_management/public/utils/page_loader/page_loader.test.ts create mode 100644 x-pack/plugins/beats_management/public/utils/page_loader/page_loader.ts create mode 100644 x-pack/plugins/beats_management/public/utils/page_loader/readme.md rename x-pack/plugins/beats_management/server/lib/{domains => }/beats.ts (88%) delete mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts delete mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts delete mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts delete mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts delete mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts create mode 100644 x-pack/plugins/beats_management/server/lib/framework.ts rename x-pack/plugins/beats_management/server/lib/{domains => }/tags.ts (89%) rename x-pack/plugins/beats_management/server/lib/{domains => }/tokens.ts (81%) rename x-pack/plugins/beats_management/server/lib/{lib.ts => types.ts} (63%) diff --git a/x-pack/package.json b/x-pack/package.json index 28cdee329a8a9..1c6ef3ed7c28f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -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" diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts index 50851dcef947e..e111d32d41802 100644 --- a/x-pack/plugins/beats_management/common/constants/index.ts +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -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'; diff --git a/x-pack/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts index dc7cd85300341..e30109bf269c7 100644 --- a/x-pack/plugins/beats_management/common/constants/plugin.ts +++ b/x-pack/plugins/beats_management/common/constants/plugin.ts @@ -7,3 +7,4 @@ export const PLUGIN = { ID: 'beats_management', }; +export const CONFIG_PREFIX = 'xpack.beats'; diff --git a/x-pack/plugins/beats_management/common/constants/security.ts b/x-pack/plugins/beats_management/common/constants/security.ts new file mode 100644 index 0000000000000..4d72e3c9f2ac7 --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/security.ts @@ -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'; diff --git a/x-pack/plugins/beats_management/index.ts b/x-pack/plugins/beats_management/index.ts index a415395de5262..3355bc9881811 100644 --- a/x-pack/plugins/beats_management/index.ts +++ b/x-pack/plugins/beats_management/index.ts @@ -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); }, }); diff --git a/x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx similarity index 69% rename from x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx rename to x-pack/plugins/beats_management/public/components/enroll_beats.tsx index c417a8860a29d..dba4e243e16d3 100644 --- a/x-pack/plugins/beats_management/public/pages/main/enroll_fragment.tsx +++ b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx @@ -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, RouteComponentProps { - 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; + createEnrollmentToken(): Promise; + onBeatEnrolled(enrolledBeat: CMBeat): void; } -export class EnrollBeat extends React.Component { + +interface ComponentState { + enrolledBeat: CMBeat | null; + hasPolledForBeat: boolean; + command: string; + beatType: string; +} + +export class EnrollBeat extends React.Component { 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 => { + public pingForBeatWithToken = async (token: string): Promise => { 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 { 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 { /> {`//`} {window.location.host} - {this.props.libs.framework.baseURLPath - ? this.props.libs.framework.baseURLPath - : ''}{' '} - {this.props.urlState.enrollmentToken} + {this.props.frameworkBasePath} {this.props.enrollmentToken}
@@ -309,38 +275,9 @@ export class EnrollBeat extends React.Component { />

- {actions.map(action => ( - { - 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} - - ))} )} ); } } - -export const EnrollBeatPageUi = withUrlState(EnrollBeat); - -export const EnrollBeatPage = injectI18n(EnrollBeatPageUi); diff --git a/x-pack/plugins/beats_management/public/app.d.ts b/x-pack/plugins/beats_management/public/components/layouts/background.tsx similarity index 56% rename from x-pack/plugins/beats_management/public/app.d.ts rename to x-pack/plugins/beats_management/public/components/layouts/background.tsx index 4e806e12d3843..3368b0e05598e 100644 --- a/x-pack/plugins/beats_management/public/app.d.ts +++ b/x-pack/plugins/beats_management/public/components/layouts/background.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export type FlatObject = { [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; +`; diff --git a/x-pack/plugins/beats_management/public/components/layouts/header.tsx b/x-pack/plugins/beats_management/public/components/layouts/header.tsx deleted file mode 100644 index 4ad567b73fc77..0000000000000 --- a/x-pack/plugins/beats_management/public/components/layouts/header.tsx +++ /dev/null @@ -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 { - public render() { - const { breadcrumbs = [] } = this.props; - - return ( - - - - - - ); - } -} - -const HeaderWrapper = styled(EuiHeader)` - height: 29px; -`; diff --git a/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx b/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx index 8f31b90ff507e..8f30803724cc4 100644 --- a/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx +++ b/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx @@ -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 = withRouter( - ({ actionSection, title, modalRender, modalClosePath, children, history }) => { - const modalContent = modalRender && modalRender(); + ({ actionSection, title, modalClosePath, children, history }) => { return ( @@ -44,18 +40,6 @@ export const NoDataLayout: React.SFC = withRouter( - {modalContent && ( - - { - history.push(modalClosePath); - }} - style={{ width: '640px' }} - > - {modalContent} - - - )} ); } diff --git a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx index 516ad648eac8b..bf192b3f1cc0c 100644 --- a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx +++ b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx @@ -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 { + private actionSection: (() => JSX.Element) | null = null; + constructor(props: PrimaryLayoutProps) { + super(props); + } -export const PrimaryLayout: React.SFC = withRouter( - ({ actionSection, title, modalRender, modalClosePath, children, history }) => { - const modalContent = modalRender && modalRender(); + public render() { + const children: (callback: RenderCallback) => void | ReactNode = this.props.children as any; return ( - - - - - -

{title}

-
-
- {actionSection} -
- - {children} - -
- {modalContent && ( - - { - history.push(modalClosePath); - }} - style={{ width: '640px' }} - > - {modalContent} - - + + {!this.props.hideBreadcrumbs && ( + + {({ breadcrumbs }) => ( + + + + + + )} + )} -
+ + + + + +

{this.props.title}

+
+
+ + {(this.actionSection && this.actionSection()) || this.props.actionSection} + +
+ + + {(children && typeof children === 'function' + ? children(this.renderAction) + : children) || } + + +
+
+ ); } -) as any; + + private renderAction = (component: () => JSX.Element) => { + this.actionSection = component; + this.forceUpdate(); + }; +} + +const HeaderWrapper = styled(EuiHeader)` + height: 29px; +`; diff --git a/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx b/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx index 32cfd4cb43316..f4fd804996927 100644 --- a/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx +++ b/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx @@ -22,7 +22,6 @@ interface LayoutProps { walkthroughSteps: Array<{ id: string; name: string; - disabled: boolean; }>; activePath: string; } diff --git a/x-pack/plugins/beats_management/public/components/loading.tsx b/x-pack/plugins/beats_management/public/components/loading.tsx new file mode 100644 index 0000000000000..f1c2455ec85b9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/loading.tsx @@ -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<{}> = () => ( + + + + + +); diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx new file mode 100644 index 0000000000000..3e704ed8f246e --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx @@ -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 { + 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 ; + } +} + +interface BreadcrumbProps extends RouteProps { + title: string; + path: string; + parentBreadcrumbs?: BreadcrumbData[]; +} + +export const Breadcrumb: React.SFC = ({ title, path, parentBreadcrumbs }) => ( + + {context => ( + + )} + +); diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/consumer.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/consumer.tsx similarity index 100% rename from x-pack/plugins/beats_management/public/components/route_with_breadcrumb/consumer.tsx rename to x-pack/plugins/beats_management/public/components/navigation/breadcrumb/consumer.tsx diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/context.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/context.tsx similarity index 100% rename from x-pack/plugins/beats_management/public/components/route_with_breadcrumb/context.tsx rename to x-pack/plugins/beats_management/public/components/navigation/breadcrumb/context.tsx diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/index.ts b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts similarity index 84% rename from x-pack/plugins/beats_management/public/components/route_with_breadcrumb/index.ts rename to x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts index 56192af8bdc0d..bfab26d8ecf59 100644 --- a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/index.ts +++ b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts @@ -6,4 +6,4 @@ export { BreadcrumbProvider } from './provider'; export { BreadcrumbConsumer } from './consumer'; -export { RouteWithBreadcrumb } from './route_with_breadcrumb'; +export { Breadcrumb } from './breadcrumb'; diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/provider.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx similarity index 100% rename from x-pack/plugins/beats_management/public/components/route_with_breadcrumb/provider.tsx rename to x-pack/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/types.d.ts b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/types.d.ts similarity index 100% rename from x-pack/plugins/beats_management/public/components/route_with_breadcrumb/types.d.ts rename to x-pack/plugins/beats_management/public/components/navigation/breadcrumb/types.d.ts diff --git a/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx b/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx new file mode 100644 index 0000000000000..189d7b1d2a3bd --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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; + 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 ( + + {routes.map(route => ( + { + const Component = route.component; + return ; + }} + /> + ))} + + ); +}; diff --git a/x-pack/plugins/beats_management/public/components/connected_link.tsx b/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx similarity index 96% rename from x-pack/plugins/beats_management/public/components/connected_link.tsx rename to x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx index b2c0e8ad607af..30d12c9ce10de 100644 --- a/x-pack/plugins/beats_management/public/components/connected_link.tsx +++ b/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx @@ -13,6 +13,7 @@ export function ConnectedLinkComponent({ path, query, disabled, + children, ...props }: { location: any; @@ -30,7 +31,7 @@ export function ConnectedLinkComponent({ return ( diff --git a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/route_with_breadcrumb.tsx b/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/route_with_breadcrumb.tsx deleted file mode 100644 index f0b5e93838b08..0000000000000 --- a/x-pack/plugins/beats_management/public/components/route_with_breadcrumb/route_with_breadcrumb.tsx +++ /dev/null @@ -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 = ({ - title, - render, - component: RouteComponent, - parentBreadcrumbs, - ...props -}) => ( - { - return ( - - {context => ( - - {render && render(renderProps)} - {RouteComponent && } - - )} - - ); - }} - /> -); diff --git a/x-pack/plugins/beats_management/public/components/table/controls.tsx b/x-pack/plugins/beats_management/public/components/table/controls.tsx index 20211ab3ced04..f484da2fa013b 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -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 { diff --git a/x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx similarity index 100% rename from x-pack/plugins/beats_management/public/components/table_controls/action_control.tsx rename to x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx diff --git a/x-pack/plugins/beats_management/public/components/table_controls/index.ts b/x-pack/plugins/beats_management/public/components/table/controls/index.ts similarity index 100% rename from x-pack/plugins/beats_management/public/components/table_controls/index.ts rename to x-pack/plugins/beats_management/public/components/table/controls/index.ts diff --git a/x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx similarity index 99% rename from x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx rename to x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx index df18dcb3ebfaa..3ce727df6bfa1 100644 --- a/x-pack/plugins/beats_management/public/components/table_controls/option_control.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx @@ -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'; diff --git a/x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx b/x-pack/plugins/beats_management/public/components/table/controls/tag_assignment.tsx similarity index 92% rename from x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx rename to x-pack/plugins/beats_management/public/components/table/controls/tag_assignment.tsx index 952636d9b9804..82f7427103cfe 100644 --- a/x-pack/plugins/beats_management/public/components/table_controls/tag_assignment.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/tag_assignment.tsx @@ -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; diff --git a/x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx b/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx similarity index 94% rename from x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx rename to x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx index f419c086a43fe..97255801c9028 100644 --- a/x-pack/plugins/beats_management/public/components/table_controls/tag_badge_list.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx @@ -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 { diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx index 1ee969e03ae87..95b169d1254f8 100644 --- a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -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) => ( - {name} + {name} ), 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', diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx index 0c421619ab07c..c1f557c82243c 100644 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx @@ -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, diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx index 2f7e983bcdbe6..98fb2df4cdf62 100644 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx @@ -17,11 +17,9 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFormRow, - // @ts-ignore EuiHorizontalRule, // @ts-ignore EuiSearchBar, - // @ts-ignore EuiSelect, // @ts-ignore EuiTabbedContent, diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx index d2be9c57abf4a..fad7d346869ed 100644 --- a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx @@ -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>; - 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 { +export class TagEdit extends React.PureComponent { constructor(props: TagEditProps) { super(props); @@ -58,7 +55,7 @@ class TagEditUi extends React.PureComponent { } public render() { - const { tag, attachedBeats, intl } = this.props; + const { tag, attachedBeats } = this.props; return (
@@ -99,18 +96,16 @@ class TagEditUi extends React.PureComponent { 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)', })} /> - {this.props.mode === 'create' && ( + {!this.props.onDetachBeat && ( @@ -236,10 +231,8 @@ class TagEditUi extends React.PureComponent { } 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 { 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); diff --git a/x-pack/plugins/beats_management/public/config_schemas.ts b/x-pack/plugins/beats_management/public/config_schemas.ts index 379e98c89be06..ef6c83265273c 100644 --- a/x-pack/plugins/beats_management/public/config_schemas.ts +++ b/x-pack/plugins/beats_management/public/config_schemas.ts @@ -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[] = [ { diff --git a/x-pack/plugins/beats_management/public/config_schemas_translations_map.ts b/x-pack/plugins/beats_management/public/config_schemas_translations_map.ts index fa2fcdacb61b6..faec5a32d86d4 100644 --- a/x-pack/plugins/beats_management/public/config_schemas_translations_map.ts +++ b/x-pack/plugins/beats_management/public/config_schemas_translations_map.ts @@ -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 => { diff --git a/x-pack/plugins/beats_management/public/containers/beats.ts b/x-pack/plugins/beats_management/public/containers/beats.ts new file mode 100644 index 0000000000000..cd29a65191c75 --- /dev/null +++ b/x-pack/plugins/beats_management/public/containers/beats.ts @@ -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 { + 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 })); + } +} diff --git a/x-pack/plugins/beats_management/public/containers/tags.ts b/x-pack/plugins/beats_management/public/containers/tags.ts new file mode 100644 index 0000000000000..4210ddc9c0831 --- /dev/null +++ b/x-pack/plugins/beats_management/public/containers/tags.ts @@ -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 { + 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; + }; +} diff --git a/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx index 596d49f2be470..4fbf65653a404 100644 --- a/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx @@ -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 { diff --git a/x-pack/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/plugins/beats_management/public/containers/with_url_state.tsx index 1630c2c20cda8..0802b4d8ea7e8 100644 --- a/x-pack/plugins/beats_management/public/containers/with_url_state.tsx +++ b/x-pack/plugins/beats_management/public/containers/with_url_state.tsx @@ -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 = (previousState: T) => T; @@ -88,7 +88,9 @@ export class WithURLStateComponent extends React.Compon } export const WithURLState = withRouter(WithURLStateComponent); -export function withUrlState(UnwrappedComponent: React.ComponentType): React.SFC { +export function withUrlState( + UnwrappedComponent: React.ComponentType +): React.SFC { return (origProps: OP) => { return ( diff --git a/x-pack/plugins/beats_management/public/frontend_types.d.ts b/x-pack/plugins/beats_management/public/frontend_types.d.ts new file mode 100644 index 0000000000000..bcaac2b3781aa --- /dev/null +++ b/x-pack/plugins/beats_management/public/frontend_types.d.ts @@ -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 = { [Key in keyof T]: string }; + +export interface AppURLState { + beatsKBar?: string; + tagsKBar?: string; + enrollmentToken?: string; + createdTag?: string; +} + +export interface RouteConfig { + path: string; + component: React.ComponentType; + routes?: RouteConfig[]; +} + +export interface AppPageProps extends URLStateProps, RouteComponentProps { + libs: FrontendLibs; + containers: { + beats: BeatsContainer; + tags: TagsContainer; + }; + routes?: RouteConfig[]; +} diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx index 1c2fb15fdc5fd..f8273e7b0ab10 100644 --- a/x-pack/plugins/beats_management/public/index.tsx +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -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( - - - - - - - +async function startApp(libs: FrontendLibs) { + libs.framework.renderUIAtPath( + BASE_PATH, + + + + + + + {(beats: BeatsContainer, tags: TagsContainer) => ( + + + + )} + + + + + + , + 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()); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index 8649bf9c37e0e..f2a020618e2e8 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -24,7 +24,7 @@ export class RestBeatsAdapter implements CMBeatsAdapter { return beat; } - public async getAll(ESQuery?: any): Promise { + public async getAll(ESQuery?: string): Promise { return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents/all', { ESQuery })).beats; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts index d987a18137116..e91005795a186 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts @@ -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'; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts new file mode 100644 index 0000000000000..ac03e9f13d6c2 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts @@ -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; + renderUIAtPath(path: string, component: React.ReactElement): 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 {} + +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 {} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index 1db905845e936..33cf335b0cf77 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -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; + public get info() { + if (this.xpackInfo) { + return this.xpackInfo; + } else { + throw new Error('framework adapter must have init called before anything else'); + } + } - private management: any; + public get currentUser() { + return this.shieldUser!; + } + private xpackInfo: FrameworkInfo | null = null; private adapterService: KibanaAdapterServiceProvider; - private rootComponent: React.ReactElement | null = null; - private uiModule: IModule; - private routes: any; - private XPackInfoProvider: any; - private xpackInfo: null | any; - private chrome: any; - private shieldUser: any; - + private shieldUser: FrameworkUser | null = null; + private settingSubscription: any; constructor( - uiModule: IModule, - management: any, - routes: any, - chrome: any, - XPackInfoProvider: any + private readonly PLUGIN_ID: string, + private readonly management: ManagementAPI, + private readonly routes: UIRoutes, + private readonly getBasePath: () => string, + private readonly onKibanaReady: () => Promise, + private readonly XPackInfoProvider: unknown, + private readonly uiSettings: any ) { this.adapterService = new KibanaAdapterServiceProvider(); - this.management = management; - this.uiModule = uiModule; - this.routes = routes; - this.chrome = chrome; - this.XPackInfoProvider = XPackInfoProvider; - this.appState = {}; + + this.settingSubscription = uiSettings.getUpdate$().subscribe({ + next: ({ key, newValue }: { key: string; newValue: boolean }) => { + if (key === 'k7design' && this.xpackInfo) { + this.xpackInfo.k7Design = newValue; + } + }, + }); } - public get baseURLPath(): string { - return this.chrome.getBasePath(); + // 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) => { - this.rootComponent = component; - }; + public async waitUntilFrameworkReady(): Promise { + const $injector = await this.onKibanaReady(); + const Private: any = $injector.get('Private'); - public hasValidLicense() { - if (!this.xpackInfo) { - return false; + let xpackInfo: any; + try { + xpackInfo = Private(this.XPackInfoProvider); + } catch (e) { + xpackInfo = false; } - return this.xpackInfo.get('features.beats_management.licenseValid', false); - } - public licenseExpired() { - if (!this.xpackInfo) { - return 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)}`); } - return this.xpackInfo.get('features.beats_management.licenseExpired', false); - } - public securityEnabled() { - if (!this.xpackInfo) { - return false; + 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; - return this.xpackInfo.get('features.beats_management.securityEnabled', false); - } + try { + this.shieldUser = await $injector.get('ShieldUser').getCurrent().$promise; + const assertUser = RuntimeFrameworkUser.decode(this.shieldUser); - public getDefaultUserRoles() { - if (!this.xpackInfo) { - return []; + if (assertUser.isLeft()) { + throw new Error( + `Error parsing user info in ${this.PLUGIN_ID}, ${PathReporter.report(assertUser)[0]}` + ); + } + } catch (e) { + this.shieldUser = null; } + } - return this.xpackInfo.get('features.beats_management.defaultUserRoles'); + public renderUIAtPath( + path: string, + component: React.ReactElement, + 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}>
` + : ` +
+ `, + // 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 getCurrentUser() { - try { - return this.shieldUser; - } catch (e) { - return null; + 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 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 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: - '
', - 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, elem); - adapter.manageAngularLifecycle($scope, $route, elem); - }); - $scope.$onInit = () => { - $scope.topNavMenu = []; - }; - } - }, - }); - }; } // tslint:disable-next-line: max-classes-per-file diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts index e9d9bf551f739..c13d9fdc2dfe6 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts @@ -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(url: string, query?: FlatObject): Promise; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts index 690843bbb1cf8..dbd9a5a5e66a1 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts @@ -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; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts index 395c01f259dc3..bcebba0c880a8 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -8,6 +8,6 @@ import { BeatTag } from '../../../../common/domain_types'; export interface CMTagsAdapter { getTagsWithIds(tagIds: string[]): Promise; delete(tagIds: string[]): Promise; - getAll(): Promise; + getAll(ESQuery?: string): Promise; upsertTag(tag: BeatTag): Promise; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts index 86daefb47c653..d44f7e1f34f97 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts @@ -23,7 +23,7 @@ export class MemoryTagsAdapter implements CMTagsAdapter { return true; } - public async getAll() { + public async getAll(ESQuery?: string) { return this.tagsDB; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts index e49d4a9109984..381b320fffe7f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -16,8 +16,8 @@ export class RestTagsAdapter implements CMTagsAdapter { return tags; } - public async getAll(): Promise { - return await this.REST.get(`/api/beats/tags`); + public async getAll(ESQuery: string): Promise { + return await this.REST.get(`/api/beats/tags`, { ESQuery }); } public async delete(tagIds: string[]): Promise { diff --git a/x-pack/plugins/beats_management/public/lib/beats.ts b/x-pack/plugins/beats_management/public/lib/beats.ts index f676f4611be63..4ff5f7e959d74 100644 --- a/x-pack/plugins/beats_management/public/lib/beats.ts +++ b/x-pack/plugins/beats_management/public/lib/beats.ts @@ -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 { const beat = await this.adapter.get(id); return beat ? (await this.mergeInTags([beat]))[0] : null; } - public async getBeatWithToken(enrollmentToken: string): Promise { + /** Get a single beat using the token it was enrolled in for lookup */ + public getBeatWithToken = async (enrollmentToken: string): Promise => { const beat = await this.adapter.getBeatWithToken(enrollmentToken); return beat; - } + }; - public async getBeatsWithTag(tagId: string): Promise { + /** Get an array of beats that have a given tag id assigned to it */ + public getBeatsWithTag = async (tagId: string): Promise => { const beats = await this.adapter.getBeatsWithTag(tagId); return await this.mergeInTags(beats); - } + }; - public async getAll(ESQuery?: any): Promise { + // 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 => { const beats = await this.adapter.getAll(ESQuery); return await this.mergeInTags(beats); - } + }; - public async update(id: string, beatData: Partial): Promise { + /** Update a given beat via it's ID */ + public update = async (id: string, beatData: Partial): Promise => { return await this.adapter.update(id, beatData); - } + }; - public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + /** unassign tags from beats using an array of tags and beats */ + public removeTagsFromBeats = async ( + removals: BeatsTagAssignment[] + ): Promise => { return await this.adapter.removeTagsFromBeats(removals); - } + }; - public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + /** assign tags from beats using an array of tags and beats */ + public assignTagsToBeats = async ( + assignments: BeatsTagAssignment[] + ): Promise => { return await this.adapter.assignTagsToBeats(assignments); - } + }; - private async mergeInTags(beats: CMBeat[]): Promise { + /** method user to join tags to beats, thus fully populating the beats */ + private mergeInTags = async (beats: CMBeat[]): Promise => { 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; - } + }; } diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index 9f2b7a3d88b0b..e1a82da8fb493 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -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; } diff --git a/x-pack/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/plugins/beats_management/public/lib/compose/memory.ts index cf1d4a9aa003e..6767c72801cb3 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/memory.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/memory.ts @@ -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 = { - tags, - tokens, - beats, - }; const pluginUIModule = uiModules.get('app/beats_management'); - const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes, null, null); + const framework = new FrameworkLib( + new KibanaFrameworkAdapter( + pluginUIModule, + management, + routes, + () => '', + onKibanaReady, + null, + null + ) + ); const libs: FrontendLibs = { - ...domainLibs, - elasticsearch: new ElasticsearchLib(esAdapter), framework, + elasticsearch: new ElasticsearchLib(esAdapter), + tags, + tokens, + beats, }; return libs; } diff --git a/x-pack/plugins/beats_management/public/lib/framework.ts b/x-pack/plugins/beats_management/public/lib/framework.ts new file mode 100644 index 0000000000000..7050a40aed5ed --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/framework.ts @@ -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(this.currentUser, 'roles', [])).length < roles.length; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/tags.ts b/x-pack/plugins/beats_management/public/lib/tags.ts index 86b21bca24310..82feb2093eda4 100644 --- a/x-pack/plugins/beats_management/public/lib/tags.ts +++ b/x-pack/plugins/beats_management/public/lib/tags.ts @@ -20,8 +20,10 @@ export class TagsLib { public async delete(tagIds: string[]): Promise { return await this.adapter.delete(tagIds); } - public async getAll(): Promise { - 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 { + return this.jsonConfigToUserYaml(await this.adapter.getAll(ESQuery)); } public async upsertTag(tag: BeatTag): Promise { tag.id = tag.id.replace(' ', '-'); diff --git a/x-pack/plugins/beats_management/public/lib/lib.ts b/x-pack/plugins/beats_management/public/lib/types.ts similarity index 73% rename from x-pack/plugins/beats_management/public/lib/lib.ts rename to x-pack/plugins/beats_management/public/lib/types.ts index 23e4da5c6336e..c8ced7d26029a 100644 --- a/x-pack/plugins/beats_management/public/lib/lib.ts +++ b/x-pack/plugins/beats_management/public/lib/types.ts @@ -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): 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 { diff --git a/x-pack/plugins/beats_management/public/pages/404.tsx b/x-pack/plugins/beats_management/public/pages/__404.tsx similarity index 100% rename from x-pack/plugins/beats_management/public/pages/404.tsx rename to x-pack/plugins/beats_management/public/pages/__404.tsx diff --git a/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx b/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx deleted file mode 100644 index d7cb47808a119..0000000000000 --- a/x-pack/plugins/beats_management/public/pages/beat/action_section.tsx +++ /dev/null @@ -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) => ( -
- {beat ? ( - - - - {beat.type} }} - /> - - - - - {beat.version} }} - /> - - - {/* TODO: We need a populated field before we can run this code - - - Uptime: 12min. - - */} - {beat.full_tags && beat.full_tags.length > 0 && ( - - - - {moment( - first(sortByOrder(beat.full_tags, 'last_updated')).last_updated - ).fromNow()} - - ), - }} - /> - - - )} - - ) : ( -
- -
- )} -
-); diff --git a/x-pack/plugins/beats_management/public/pages/beat/activity.tsx b/x-pack/plugins/beats_management/public/pages/beat/activity.tsx deleted file mode 100644 index 6a031ef195200..0000000000000 --- a/x-pack/plugins/beats_management/public/pages/beat/activity.tsx +++ /dev/null @@ -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) => ( -
- -
-); diff --git a/x-pack/plugins/beats_management/public/pages/beat/detail.tsx b/x-pack/plugins/beats_management/public/pages/beat/details.tsx similarity index 88% rename from x-pack/plugins/beats_management/public/pages/beat/detail.tsx rename to x-pack/plugins/beats_management/public/pages/beat/details.tsx index 6eee9ce738703..0a5d17d84e0f2 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/detail.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/details.tsx @@ -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 { const { beat, intl } = props; if (!beat) { return ( -
- -
+ ); } const configurationBlocks = flatten( @@ -126,6 +127,14 @@ class BeatDetailPageUi extends React.PureComponent { ]; return ( + + diff --git a/x-pack/plugins/beats_management/public/pages/beat/index.tsx b/x-pack/plugins/beats_management/public/pages/beat/index.tsx index f33d926d68a10..b47b1558abad5 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/index.tsx @@ -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 { - 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 { + 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 ? ( + + + + {beat.type} }} + /> + + + + + {beat.version} }} + /> + + + {beat.full_tags && beat.full_tags.length > 0 && ( + + + + {moment( + first(sortByOrder(beat.full_tags, 'last_updated')).last_updated + ).fromNow()} + + ), + }} + /> + + + )} + + ) : ( + + ); + } + 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 ( - }> - - {tabs.map((tab, index) => ( + + + + { - 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} + - ))} - - - - } - /> - ( - this.loadBeat()} - {...props} + + - )} - /> - ( - - )} - /> - + + + {!this.state.beat &&
Beat not found
} + {this.state.beat && ( + + + {id && } />} + + )} +
); } + 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(BeatDetailsPageComponent); -export const BeatDetailsPage = injectI18n(BeatDetailsPageUi); +export const BeatDetailsPage = injectI18n(BeatDetailsPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx index 9d0a693e8efa7..a6c7c01ee469d 100644 --- a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx +++ b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx @@ -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 + + this.setState({ notifications: [] })} toastLifeTimeMs={5000} /> - + ); } - - private getBeat = async () => { - try { - const beat = await this.props.libs.beats.get(this.props.beatId); - this.setState({ beat }); - } catch (e) { - throw new Error(e); - } - }; } diff --git a/x-pack/plugins/beats_management/public/pages/enforce_security.tsx b/x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx similarity index 92% rename from x-pack/plugins/beats_management/public/pages/enforce_security.tsx rename to x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx index a79f828a28fb3..aab2a965fd04d 100644 --- a/x-pack/plugins/beats_management/public/pages/enforce_security.tsx +++ b/x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx @@ -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 }) => ( ( ( - - - ); - } -} diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx deleted file mode 100644 index bc9b9922f6502..0000000000000 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ /dev/null @@ -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 { - libs: FrontendLibs; - location: any; - intl: InjectedIntl; -} - -interface MainPagesState { - enrollBeat?: { - enrollmentToken: string; - } | null; - beats: CMPopulatedBeat[]; - unfilteredBeats: CMPopulatedBeat[]; - loadedBeatsAtLeastOnce: boolean; -} - -class MainPagesComponent extends React.PureComponent { - 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 ; - } - const tabs = [ - { - id: '/overview/beats', - name: ( - - ), - disabled: false, - }, - // { - // id: '/overview/activity', - // name: 'Beats Activity', - // disabled: false, - // }, - { - id: '/overview/tags', - name: ( - - ), - 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 ( - - - - - - } - > -

- -

-
- ); - } - - if (this.props.location.pathname.includes('/overview/initial')) { - return ( - - - {walkthroughSteps.map(step => ( - ( - - )} - /> - ))} - - - ); - } - - const renderedTabs = tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.props.location.pathname} - disabled={tab.disabled} - key={index} - > - {tab.name} - - )); - - return ( - - ( - - )} - /> - ( - - )} - /> - - } - > - {renderedTabs} - - ( - - )} - /> - ( - - )} - /> - } - /> - - ); - } - - 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(MainPagesComponent); - -export const MainPages = injectI18n(MainPagesUi); diff --git a/x-pack/plugins/beats_management/public/pages/main/tags.tsx b/x-pack/plugins/beats_management/public/pages/main/tags.tsx deleted file mode 100644 index ec2171c1104b1..0000000000000 --- a/x-pack/plugins/beats_management/public/pages/main/tags.tsx +++ /dev/null @@ -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 { - libs: FrontendLibs; - intl: InjectedIntl; -} - -interface TagsPageState { - tags: BeatTag[]; - tableRef: any; -} - -class TagsPageUi extends React.PureComponent { - public static ActionArea = ({ goTo }: TagsPageProps) => ( - { - goTo('/tag/create'); - }} - > - - - ); - - constructor(props: TagsPageProps) { - super(props); - - this.state = { - tags: [], - tableRef: React.createRef(), - }; - - this.loadTags(); - } - - public render() { - return ( - - {autocompleteProps => ( -
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} - /> - )} - - ); - } - - 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']; -}; diff --git a/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx b/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx new file mode 100644 index 0000000000000..ce5328a219ce1 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx @@ -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 { + 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 = () => ( + { + this.props.goTo('/tag/create'); + }} + > + + + ); + + public render() { + return ( + + + + {autocompleteProps => ( +
{ + 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} + /> + )} + + + ); + } + + 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); diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx similarity index 57% rename from x-pack/plugins/beats_management/public/pages/main/beats.tsx rename to x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx index c6c24542b6e4c..8d1ce681132d3 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx @@ -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 { +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, RouteComponentProps { - libs: FrontendLibs; -} +class BeatsPageComponent extends React.PureComponent { + private tableRef: React.RefObject = React.createRef(); + constructor(props: PageProps) { + super(props); + + this.state = { + notifications: [], + tags: null, + }; -class BeatsPageUi extends React.PureComponent { - public static ActionArea = (props: ActionAreaProps) => ( + if (props.urlState.beatsKBar) { + props.containers.beats.reload(props.urlState.beatsKBar); + } + props.renderAction(this.renderActionArea); + } + + public renderActionArea = () => ( { @@ -71,7 +76,7 @@ class BeatsPageUi extends React.PureComponent { size="s" color="primary" onClick={async () => { - props.goTo(`/overview/beats/enroll`); + this.props.goTo(`/overview/enrolled_beats/enroll`); }} > { /> - {props.location.pathname === '/overview/beats/enroll' && ( + {this.props.location.pathname === '/overview/enrolled_beats/enroll' && ( { - 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 { - + { + const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken(); + this.props.setUrlState({ + enrollmentToken, + }); + }} + onBeatEnrolled={() => { + this.props.setUrlState({ + enrollmentToken: '', + }); + }} + /> + {!this.props.urlState.enrollmentToken && ( + + { + this.props.goTo('/overview/enrolled_beats'); + }} + > + Done + + + )} )} ); - 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 ( -
+ + {autocompleteProps => (
{ 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 { dismissToast={() => this.setState({ notifications: [] })} toastLifeTimeMs={5000} /> - + ); } - 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 { 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 { 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 { 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 { 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 { }; 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 { }, { 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 { }, { 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 { 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 { }); }; - 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 { 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 { .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); diff --git a/x-pack/plugins/beats_management/public/pages/overview/index.tsx b/x-pack/plugins/beats_management/public/pages/overview/index.tsx new file mode 100644 index 0000000000000..a50c3088bc929 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/overview/index.tsx @@ -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 { + constructor(props: AppPageProps) { + super(props); + this.state = { + loadedBeatsAtLeastOnce: false, + beats: [], + }; + } + public onTabClicked = (path: string) => { + return () => { + this.props.goTo(path); + }; + }; + + public render() { + return ( + + {(renderAction: any) => ( + + {(beats: BeatsContainer, tags: TagsContainer) => ( + + + + + + + + + + + + )} + + )} + + ); + } +} + +export const MainPage = withUrlState(MainPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/tag/index.tsx b/x-pack/plugins/beats_management/public/pages/tag.tsx similarity index 78% rename from x-pack/plugins/beats_management/public/pages/tag/index.tsx rename to x-pack/plugins/beats_management/public/pages/tag.tsx index 88f909514763f..29eabe49dea55 100644 --- a/x-pack/plugins/beats_management/public/pages/tag/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/tag.tsx @@ -4,35 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -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 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 { 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 { - 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 { +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 { - 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[]} /> @@ -117,7 +116,7 @@ export class TagPageComponent extends React.PureComponent - this.props.goTo('/overview/tags')}> + this.props.goTo('/overview/configuration_tags')}> { 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 { 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(TagPageComponent); -export const TagPage = injectI18n(TagPageUi); +export const TagPage = injectI18n(TagPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx new file mode 100644 index 0000000000000..0ceed78fc3f21 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx @@ -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 { + 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 ( + + + {this.state.readyToContinue && ( + + { + this.props.goTo('/walkthrough/initial/tag'); + }} + > + Continue + + + )} + + ); + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/finish_walkthrough.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx similarity index 74% rename from x-pack/plugins/beats_management/public/pages/main/finish_walkthrough.tsx rename to x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx index b3f53dfb5a5c2..392ac808a9dbb 100644 --- a/x-pack/plugins/beats_management/public/pages/main/finish_walkthrough.tsx +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx @@ -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, RouteComponentProps { - 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 { - 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 { 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 { fill disabled={!this.state.assigned} onClick={async () => { - goTo('/overview/beats'); + goTo('/overview/enrolled_beats'); }} > { ); } - 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 { }) ); } - 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 { }; } -const FinishWalkthroughPageUi = withUrlState(FinishWalkthrough); - -export const FinishWalkthroughPage = injectI18n(FinishWalkthroughPageUi); +export const FinishWalkthroughPage = injectI18n(FinishWalkthrough); diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx new file mode 100644 index 0000000000000..faf8daafebfbd --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx @@ -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 ( + + + {' '} + + + } + > +

+ +

+
+ ); + } + return ( + { + // FIXME implament goto + }} + activePath={this.props.location.pathname} + > + + + ); + } +} +export const InitialWalkthroughPage = injectI18n(InitialWalkthroughPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx similarity index 55% rename from x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx rename to x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx index 6d649cb46b5b1..0e65e6a9e6083 100644 --- a/x-pack/plugins/beats_management/public/pages/main/create_tag_fragment.tsx +++ b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx @@ -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 { - libs: FrontendLibs; - match: any; - intl: InjectedIntl; -} - -interface TagPageState { - showFlyout: boolean; +interface PageState { tag: BeatTag; } -class CreateTagFragment extends React.PureComponent { - private mode: 'edit' | 'create' = 'create'; - constructor(props: TagPageProps) { +export class InitialTagPage extends Component { + 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 }; if (props.urlState.createdTag) { - this.mode = 'edit'; this.loadTag(); } } @@ -52,24 +37,12 @@ class CreateTagFragment extends React.PureComponent { - 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} /> - - - }; 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 this.props.setUrlState({ createdTag: newTag.id, }); - this.props.goTo(`/overview/initial/finish`); + this.props.goTo(`/walkthrough/initial/finish`); }; } -const CreateTagPageFragmentUi = withUrlState(CreateTagFragment); - -export const CreateTagPageFragment = injectI18n(CreateTagPageFragmentUi); diff --git a/x-pack/plugins/beats_management/public/router.tsx b/x-pack/plugins/beats_management/public/router.tsx index 1e1531d4d9269..febf5d73ed096 100644 --- a/x-pack/plugins/beats_management/public/router.tsx +++ b/x-pack/plugins/beats_management/public/router.tsx @@ -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 ( - -
- - {({ breadcrumbs }) => ( -
{ + 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 ; + } + + return ( + + {/* Redirects mapping */} + + {/* License check (UI displays when license exists but is expired) */} + {get(this.props.libs.framework.info, 'license.expired', true) && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } /> )} - - - {libs.framework.licenseExpired() && } />} - {!libs.framework.securityEnabled() && } />} - {!libs.framework.getCurrentUser() || - (!libs.framework.getCurrentUser().roles.includes('beats_admin') && - !libs.framework - .getDefaultUserRoles() - .some(r => libs.framework.getCurrentUser().roles.includes(r)) && ( - } /> - ))} - } - /> - } /> - { - 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) => } - /> - { - 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) && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } + /> + )} + + {/* Make sure the user has correct permissions */} + {!this.props.libs.framework.currentUserHasOneOfRoles( + ['beats_admin'].concat(this.props.libs.framework.info.settings.defaultUserRoles) + ) && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } + /> + )} + + {/* If there are no beats or tags yet, redirect to the walkthrough */} + {this.state.loadingStatus === 'loaded:empty' && ( + + !props.location.pathname.includes('/walkthrough') ? ( + + ) : 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) => } - /> + /> + )} + + {/* This app does not make use of a homepage. The mainpage is overview/enrolled_beats */} + } /> -
-
- ); -}; + + {/* Render routes from the FS */} + + {(URLProps: URLStateProps) => ( + + )} + +
+ ); + } +} diff --git a/x-pack/plugins/beats_management/public/utils/page_loader/__snapshots__/page_loader.test.ts.snap b/x-pack/plugins/beats_management/public/utils/page_loader/__snapshots__/page_loader.test.ts.snap new file mode 100644 index 0000000000000..91852239ba6fd --- /dev/null +++ b/x-pack/plugins/beats_management/public/utils/page_loader/__snapshots__/page_loader.test.ts.snap @@ -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": "*", + }, +] +`; diff --git a/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.test.ts b/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.test.ts new file mode 100644 index 0000000000000..e7618588e536e --- /dev/null +++ b/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.test.ts @@ -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('*'); + }); +}); diff --git a/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.ts b/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.ts new file mode 100644 index 0000000000000..c6ccd14dbd825 --- /dev/null +++ b/x-pack/plugins/beats_management/public/utils/page_loader/page_loader.ts @@ -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; + 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; + }, + + {} + ); + } +} diff --git a/x-pack/plugins/beats_management/public/utils/page_loader/readme.md b/x-pack/plugins/beats_management/public/utils/page_loader/readme.md new file mode 100644 index 0000000000000..cd53a6eae9901 --- /dev/null +++ b/x-pack/plugins/beats_management/public/utils/page_loader/readme.md @@ -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 diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 15619c07a795c..0aa04f04b66d3 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -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, }, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts index 22ee7a34066b3..c0cd1e561b587 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts @@ -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; get( @@ -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; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts index ba593a793e34b..9a9424fc54e4e 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -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, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.ts index b483379d444c0..9ef126ec69296 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.ts @@ -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); }, }); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 64dca03e68d56..c02df7a42030e 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -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( route: FrameworkRouteOptions ): 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 {} + +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 {} + +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 {} + export interface FrameworkAuthenticatedUser { kind: 'authenticated'; [internalAuthData]: AuthDataType; @@ -26,9 +123,6 @@ export interface FrameworkAuthenticatedUser { roles: string[]; full_name: string | null; email: string | null; - metadata: { - [key: string]: any; - }; enabled: boolean; } @@ -45,46 +139,32 @@ export type FrameworkUser = | FrameworkUnAuthenticatedUser | FrameworkInternalUser; export interface FrameworkRequest< - InternalRequest extends FrameworkWrappableRequest = FrameworkWrappableRequest + KibanaServerRequestGenaric extends Partial = any > { - user: FrameworkUser; - headers: InternalRequest['headers']; - info: InternalRequest['info']; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; + user: FrameworkUser; + 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; config?: {}; } export type FrameworkRouteHandler< - RouteRequest extends FrameworkWrappableRequest, + RouteRequest extends KibanaServerRequest, RouteResponse extends FrameworkResponse > = (request: FrameworkRequest, 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; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts index 62e8b0599097d..90500e0283511 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts @@ -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) { 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( + req: InternalRequest + ): FrameworkRequest { + 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, + }; + } } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 2004bf723f007..8a1bf22c434c2 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -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; - - 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(); + public readonly internalUser = internalUser; + public info: null | FrameworkInfo = null; - 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) { - 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(request), h); + }, method: route.method, path: route.path, config: route.config, }); } - private async getUser(request: FrameworkRequest) { + private async wrapRequest( + req: KibanaServerRequest + ): Promise> { + 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 { + 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) { - // 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.', - }; - } + private xpackInfoWasUpdatedHandler = (xpackInfo: XpackInfo) => { + let xpackInfoUnpacked: FrameworkInfo; - 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.`, - }; + // If, for some reason, we cannot get the license information + // from Elasticsearch, assume worst case and disable + if (!xpackInfo || !xpackInfo.isAvailable()) { + this.info = null; + return; } - // 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.`, + 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; } - // 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, - }; + 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; - // 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), }; - } + }; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index e3afa544fa0e1..e70d073858706 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -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, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts index 2fe8c811c396e..51d846931ed2e 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts @@ -11,7 +11,7 @@ export interface TokenEnrollmentData { } export interface CMTokensAdapter { - deleteEnrollmentToken(enrollmentToken: string): Promise; - getEnrollmentToken(enrollmentToken: string): Promise; + deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise; + getEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise; upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index 6aa9ceff46629..53038651cc4c5 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -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 { + public async getEnrollmentToken( + user: FrameworkUser, + tokenString: string + ): Promise { 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(response, '_source.enrollment_token', { expires_on: '0', token: null, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts index fabbafc040969..dcb0ab7bad0aa 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -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 { + public async getEnrollmentToken( + user: FrameworkUser, + tokenString: string + ): Promise { return new Promise(resolve => { return resolve(this.tokenDB.find(token => token.token === tokenString)); }); diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/beats.ts similarity index 88% rename from x-pack/plugins/beats_management/server/lib/domains/beats.ts rename to x-pack/plugins/beats_management/server/lib/beats.ts index a1b890119fc3e..5bcb10bd04a97 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/beats.ts @@ -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) { 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, diff --git a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts index bc00278251610..f2886e4efc323 100644 --- a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts @@ -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,16 +38,12 @@ export function compose(server: any): CMServerLibs { framework, }); - const domainLibs: CMDomainLibs = { - beats, - tags, - tokens, - }; - const libs: CMServerLibs = { framework, database, - ...domainLibs, + beats, + tags, + tokens, }; return libs; diff --git a/x-pack/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/plugins/beats_management/server/lib/compose/testing.ts index 41fc18b80aeef..7a3f5ed8b5d6a 100644 --- a/x-pack/plugins/beats_management/server/lib/compose/testing.ts +++ b/x-pack/plugins/beats_management/server/lib/compose/testing.ts @@ -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; } diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts deleted file mode 100644 index 96ff86013762f..0000000000000 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts deleted file mode 100644 index d115c49244c65..0000000000000 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts +++ /dev/null @@ -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; - - 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); - }); - }); -}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts deleted file mode 100644 index 63a98f5c054df..0000000000000 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts +++ /dev/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> = []; - 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'); - }); - }); -}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts deleted file mode 100644 index 74f747374e755..0000000000000 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/update.test.ts +++ /dev/null @@ -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; - - 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'); - }); - }); -}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts deleted file mode 100644 index 91c504cd9f503..0000000000000 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/x-pack/plugins/beats_management/server/lib/framework.ts b/x-pack/plugins/beats_management/server/lib/framework.ts new file mode 100644 index 0000000000000..3a79afa1c1ef7 --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/framework.ts @@ -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) { + 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, + 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); + }; + } +} diff --git a/x-pack/plugins/beats_management/server/lib/domains/tags.ts b/x-pack/plugins/beats_management/server/lib/tags.ts similarity index 89% rename from x-pack/plugins/beats_management/server/lib/domains/tags.ts rename to x-pack/plugins/beats_management/server/lib/tags.ts index 79ff2007d1160..01ad2e9ee0265 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats_management/server/lib/tags.ts @@ -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) {} diff --git a/x-pack/plugins/beats_management/server/lib/domains/tokens.ts b/x-pack/plugins/beats_management/server/lib/tokens.ts similarity index 81% rename from x-pack/plugins/beats_management/server/lib/domains/tokens.ts rename to x-pack/plugins/beats_management/server/lib/tokens.ts index 529a526bea75d..c6c1a06985d8f 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats_management/server/lib/tokens.ts @@ -7,24 +7,27 @@ import { timingSafeEqual } from 'crypto'; import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; import moment from 'moment'; import uuid from 'uuid'; -import { BackendFrameworkAdapter } from '../adapters/framework/adapter_types'; -import { FrameworkUser } from '../adapters/framework/adapter_types'; -import { CMTokensAdapter } from '../adapters/tokens/adapter_types'; +import { FrameworkUser } from './adapters/framework/adapter_types'; +import { CMTokensAdapter } from './adapters/tokens/adapter_types'; +import { CMServerLibs } from './types'; const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; export class CMTokensDomain { private adapter: CMTokensAdapter; - private framework: BackendFrameworkAdapter; + private framework: CMServerLibs['framework']; - constructor(adapter: CMTokensAdapter, libs: { framework: BackendFrameworkAdapter }) { + constructor(adapter: CMTokensAdapter, libs: { framework: CMServerLibs['framework'] }) { this.adapter = adapter; this.framework = libs.framework; } public async getEnrollmentToken(enrollmentToken: string) { - const fullToken = await this.adapter.getEnrollmentToken(enrollmentToken); + const fullToken = await this.adapter.getEnrollmentToken( + this.framework.internalUser, + enrollmentToken + ); if (!fullToken) { return { @@ -48,7 +51,7 @@ export class CMTokensDomain { } public async deleteEnrollmentToken(enrollmentToken: string) { - return await this.adapter.deleteEnrollmentToken(enrollmentToken); + return await this.adapter.deleteEnrollmentToken(this.framework.internalUser, enrollmentToken); } public verifyToken(recivedToken: string, token2: string, decode = true) { @@ -56,7 +59,7 @@ export class CMTokensDomain { let expired = false; if (decode) { - const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); + const enrollmentTokenSecret = this.framework.getSetting('encryptionKey'); try { verifyToken(recivedToken, enrollmentTokenSecret); @@ -96,7 +99,7 @@ export class CMTokensDomain { } public generateAccessToken() { - const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); + const enrollmentTokenSecret = this.framework.getSetting('encryptionKey'); const tokenData = { created: moment().toJSON(), @@ -111,9 +114,7 @@ export class CMTokensDomain { numTokens: number = 1 ): Promise { const tokens = []; - const enrollmentTokensTtlInSeconds = this.framework.getSetting( - 'xpack.beats.enrollmentTokensTtlInSeconds' - ); + const enrollmentTokensTtlInSeconds = this.framework.getSetting('enrollmentTokensTtlInSeconds'); const enrollmentTokenExpiration = moment() .add(enrollmentTokensTtlInSeconds, 'seconds') diff --git a/x-pack/plugins/beats_management/server/lib/lib.ts b/x-pack/plugins/beats_management/server/lib/types.ts similarity index 63% rename from x-pack/plugins/beats_management/server/lib/lib.ts rename to x-pack/plugins/beats_management/server/lib/types.ts index b8d51374741fe..d9f2a922ebb34 100644 --- a/x-pack/plugins/beats_management/server/lib/lib.ts +++ b/x-pack/plugins/beats_management/server/lib/types.ts @@ -5,25 +5,23 @@ */ import { DatabaseAdapter } from './adapters/database/adapter_types'; -import { BackendFrameworkAdapter, FrameworkUser } from './adapters/framework/adapter_types'; +import { FrameworkUser } from './adapters/framework/adapter_types'; -import { CMBeatsDomain } from './domains/beats'; -import { CMTagsDomain } from './domains/tags'; -import { CMTokensDomain } from './domains/tokens'; +import { CMBeatsDomain } from './beats'; +import { BackendFrameworkLib } from './framework'; +import { CMTagsDomain } from './tags'; +import { CMTokensDomain } from './tokens'; export type UserOrToken = FrameworkUser | string; -export interface CMDomainLibs { +export interface CMServerLibs { + framework: BackendFrameworkLib; + database?: DatabaseAdapter; beats: CMBeatsDomain; tags: CMTagsDomain; tokens: CMTokensDomain; } -export interface CMServerLibs extends CMDomainLibs { - framework: BackendFrameworkAdapter; - database?: DatabaseAdapter; -} - export enum BeatEnrollmentStatus { Success = 'Success', ExpiredEnrollmentToken = 'Expired enrollment token', diff --git a/x-pack/plugins/beats_management/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts index e278e6ff735c1..679f8f3423867 100644 --- a/x-pack/plugins/beats_management/server/management_server.ts +++ b/x-pack/plugins/beats_management/server/management_server.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMServerLibs } from './lib/lib'; +import { CMServerLibs } from './lib/types'; import { createGetBeatConfigurationRoute } from './rest_api/beats/configuration'; import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; import { createGetBeatRoute } from './rest_api/beats/get'; diff --git a/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts index 331403e8145c3..e427c9b0b1115 100644 --- a/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts +++ b/x-pack/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { HapiBackendFrameworkAdapter } from './../../lib/adapters/framework/hapi_framework_adapter'; import { testHarnes } from './test_harnes'; @@ -19,10 +19,8 @@ describe('assign_tags_to_beats', () => { beforeEach(async () => await testHarnes.loadData()); it('should add a single tag to a single beat', async () => { - const { - result, - statusCode, - } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + const { result, statusCode } = await ((serverLibs.framework as any) + .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ method: 'POST', url: '/api/beats/agents_tags/assignments', headers: { @@ -39,10 +37,8 @@ describe('assign_tags_to_beats', () => { }); it('should not re-add an existing tag to a beat', async () => { - const { - result, - statusCode, - } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + const { result, statusCode } = await ((serverLibs.framework as any) + .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ method: 'POST', url: '/api/beats/agents_tags/assignments', headers: { @@ -70,10 +66,8 @@ describe('assign_tags_to_beats', () => { }); it('should add a single tag to a multiple beats', async () => { - const { - result, - statusCode, - } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + const { result, statusCode } = await ((serverLibs.framework as any) + .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ method: 'POST', url: '/api/beats/agents_tags/assignments', headers: { @@ -114,10 +108,8 @@ describe('assign_tags_to_beats', () => { }); it('should add multiple tags to a single beat', async () => { - const { - result, - statusCode, - } = await (serverLibs.framework as HapiBackendFrameworkAdapter).injectRequstForTesting({ + const { result, statusCode } = await ((serverLibs.framework as any) + .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ method: 'POST', url: '/api/beats/agents_tags/assignments', headers: { diff --git a/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts b/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts index ed93ffbe36e1e..590ce0bd7b287 100644 --- a/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts +++ b/x-pack/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts @@ -13,7 +13,7 @@ import { promisify } from 'util'; import { BeatTag, CMBeat } from '../../../common/domain_types'; import { TokenEnrollmentData } from '../../lib/adapters/tokens/adapter_types'; import { compose } from '../../lib/compose/testing'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { initManagementServer } from './../../management_server'; const readFileAsync = promisify(readFile); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts index a24d6dee05132..d342a293c97ea 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts @@ -6,7 +6,7 @@ import Joi from 'joi'; import { omit } from 'lodash'; import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; import { ReturnedConfigurationBlock } from './../../../common/domain_types'; diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts index 266e3baa3829f..9c2b69908febd 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts @@ -5,16 +5,16 @@ */ import Joi from 'joi'; import { omit } from 'lodash'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; -import { BeatEnrollmentStatus } from '../../lib/lib'; +import { BeatEnrollmentStatus, CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: write to Kibana audit log file +// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agent/{beatId}', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, config: { auth: false, validate: { @@ -61,7 +61,7 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ return h.response({ access_token: accessToken }).code(201); } } catch (err) { - // TODO move this to kibana route thing in adapter + // FIXME move this to kibana route thing in adapter return wrapEsError(err); } }, diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts index 49fac5e9f009c..41415521c7bca 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts @@ -5,7 +5,7 @@ */ import { CMBeat } from '../../../common/domain_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; export const createGetBeatRoute = (libs: CMServerLibs) => ({ diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts index 6dfec291f602b..1bf96fba96f98 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts @@ -5,15 +5,18 @@ */ import * as Joi from 'joi'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { CMBeat } from '../../../common/domain_types'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; export const createListAgentsRoute = (libs: CMServerLibs) => ({ method: 'GET', path: '/api/beats/agents/{listByAndValue*}', requiredRoles: ['beats_admin'], + licenseRequired: REQUIRED_LICENSES, + validate: { headers: Joi.object({ 'kbn-beats-enrollment-token': Joi.string().required(), @@ -24,7 +27,6 @@ export const createListAgentsRoute = (libs: CMServerLibs) => ({ ESQuery: Joi.string(), }), }, - licenseRequired: true, handler: async (request: FrameworkRequest) => { const listByAndValueParts = request.params.listByAndValue ? request.params.listByAndValue.split('/') @@ -56,7 +58,7 @@ export const createListAgentsRoute = (libs: CMServerLibs) => ({ return { beats }; } catch (err) { - // TODO move this to kibana route thing in adapter + // FIXME move this to kibana route thing in adapter return wrapEsError(err); } }, diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts index 91cda6e9524f9..cc40e3d8ae642 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts @@ -8,14 +8,15 @@ import Joi from 'joi'; import { BeatsTagAssignment } from '../../../public/lib/adapters/beats/adapter_types'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: write to Kibana audit log file +// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agents_tags/assignments', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, requiredRoles: ['beats_admin'], config: { validate: { diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts index d1912d96fdcdb..b38cc43fd7e05 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts @@ -5,15 +5,16 @@ */ import Joi from 'joi'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: write to Kibana audit log file +// TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agents_tags/removals', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, requiredRoles: ['beats_admin'], config: { validate: { diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts index 64e08330734d0..2e1dc51f142fd 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -5,15 +5,16 @@ */ import Joi from 'joi'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: write to Kibana audit log file (include who did the verification as well) +// TODO: write to Kibana audit log file (include who did the verification as well) https://github.com/elastic/kibana/issues/26024 export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ method: 'PUT', path: '/api/beats/agent/{beatId}', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, requiredRoles: ['beats_admin'], config: { validate: { diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts index 0f138ecf04a14..a533a1d2bace6 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMServerLibs } from '../../lib/lib'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; export const createDeleteTagsWithIdsRoute = (libs: CMServerLibs) => ({ method: 'DELETE', path: '/api/beats/tags/{tagIds}', requiredRoles: ['beats_admin'], - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, handler: async (request: any) => { const tagIdString: string = request.params.tagIds; const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts index 36d00208066c2..c7f76dbd989c9 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { BeatTag } from '../../../common/domain_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; +import { FrameworkRouteOptions } from './../../lib/adapters/framework/adapter_types'; -export const createGetTagsWithIdsRoute = (libs: CMServerLibs) => ({ +export const createGetTagsWithIdsRoute = (libs: CMServerLibs): FrameworkRouteOptions => ({ method: 'GET', path: '/api/beats/tags/{tagIds}', requiredRoles: ['beats_admin'], - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, handler: async (request: any) => { const tagIdString: string = request.params.tagIds; const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts index f255a627220d3..91bf7f92b9782 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts @@ -5,14 +5,16 @@ */ import * as Joi from 'joi'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { BeatTag } from '../../../common/domain_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; export const createListTagsRoute = (libs: CMServerLibs) => ({ method: 'GET', path: '/api/beats/tags', requiredRoles: ['beats_admin'], + licenseRequired: REQUIRED_LICENSES, validate: { headers: Joi.object({ 'kbn-beats-enrollment-token': Joi.string().required(), @@ -23,7 +25,6 @@ export const createListTagsRoute = (libs: CMServerLibs) => ({ ESQuery: Joi.string(), }), }, - licenseRequired: true, handler: async (request: any) => { let tags: BeatTag[]; try { diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts index 600eef42d90af..afc6e6b81538a 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts @@ -6,16 +6,16 @@ import Joi from 'joi'; import { get, values } from 'lodash'; -import { ConfigurationBlockTypes } from '../../../common/constants'; +import { ConfigurationBlockTypes, REQUIRED_LICENSES } from '../../../common/constants'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; // TODO: write to Kibana audit log file export const createSetTagRoute = (libs: CMServerLibs) => ({ method: 'PUT', path: '/api/beats/tag/{tag}', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, requiredRoles: ['beats_admin'], config: { validate: { diff --git a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts index 42604ced8a972..1b70bd3b3ad8f 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts @@ -6,8 +6,9 @@ import Joi from 'joi'; import { get } from 'lodash'; +import { REQUIRED_LICENSES } from 'x-pack/plugins/beats_management/common/constants'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; +import { CMServerLibs } from '../../lib/types'; import { wrapEsError } from '../../utils/error_wrappers'; // TODO: write to Kibana audit log file @@ -15,7 +16,7 @@ const DEFAULT_NUM_TOKENS = 1; export const createTokensRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/enrollment_tokens', - licenseRequired: true, + licenseRequired: REQUIRED_LICENSES, requiredRoles: ['beats_admin'], config: { validate: { diff --git a/x-pack/plugins/beats_management/server/utils/wrap_request.ts b/x-pack/plugins/beats_management/server/utils/wrap_request.ts index 83838eb12b335..57cf70a99a296 100644 --- a/x-pack/plugins/beats_management/server/utils/wrap_request.ts +++ b/x-pack/plugins/beats_management/server/utils/wrap_request.ts @@ -6,12 +6,11 @@ import { FrameworkRequest, - FrameworkWrappableRequest, + internalAuthData, + KibanaServerRequest, } from '../lib/adapters/framework/adapter_types'; -export const internalAuthData = Symbol('internalAuthData'); - -export function wrapRequest( +export function wrapRequest( req: InternalRequest ): FrameworkRequest { const { params, payload, query, headers, info } = req; diff --git a/x-pack/plugins/beats_management/wallaby.js b/x-pack/plugins/beats_management/wallaby.js index bb57d22afafb3..eb5652f154774 100644 --- a/x-pack/plugins/beats_management/wallaby.js +++ b/x-pack/plugins/beats_management/wallaby.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ const path = require('path'); -process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); +process.env.NODE_PATH = path.resolve(__dirname, '..', '..', '..', 'node_modules'); module.exports = function (wallaby) { return { @@ -23,7 +23,10 @@ module.exports = function (wallaby) { type: 'node', runner: 'node', }, - testFramework: 'jest', + testFramework: { + type: 'jest', + //path: jestPath, + }, compilers: { '**/*.ts?(x)': wallaby.compilers.typeScript({ typescript: require('typescript'), // eslint-disable-line @@ -38,7 +41,6 @@ module.exports = function (wallaby) { const path = require('path'); const kibanaDirectory = path.resolve(wallaby.localProjectDir, '..', '..', '..'); - wallaby.testFramework.configure({ rootDir: wallaby.localProjectDir, moduleNameMapper: { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f3e48f41a6688..a41ae41298e49 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -48,16 +48,20 @@ import { RandomProvider, AceEditorProvider, GrokDebuggerProvider, - } from './services'; // the default export of config files must be a config provider // that returns an object with the projects config values export default async function ({ readConfigFile }) { - - const kibanaCommonConfig = await readConfigFile(require.resolve('../../../test/common/config.js')); - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../../../test/functional/config.js')); - const kibanaAPITestsConfig = await readConfigFile(require.resolve('../../../test/api_integration/config.js')); + const kibanaCommonConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../../../test/functional/config.js') + ); + const kibanaAPITestsConfig = await readConfigFile( + require.resolve('../../../test/api_integration/config.js') + ); return { // list paths to the files that contain your plugins tests @@ -130,10 +134,7 @@ export default async function ({ readConfigFile }) { esTestCluster: { license: 'trial', from: 'snapshot', - serverArgs: [ - 'xpack.license.self_generated.type=trial', - 'xpack.security.enabled=true', - ], + serverArgs: ['xpack.license.self_generated.type=trial', 'xpack.security.enabled=true'], }, kbnTestServer: { @@ -158,47 +159,47 @@ export default async function ({ readConfigFile }) { apps: { ...kibanaFunctionalConfig.get('apps'), login: { - pathname: '/login' + pathname: '/login', }, monitoring: { - pathname: '/app/monitoring' + pathname: '/app/monitoring', }, logstashPipelines: { pathname: '/app/kibana', - hash: '/management/logstash/pipelines' + hash: '/management/logstash/pipelines', }, graph: { pathname: '/app/graph', }, grokDebugger: { pathname: '/app/kibana', - hash: '/dev_tools/grokdebugger' + hash: '/dev_tools/grokdebugger', }, spaceSelector: { pathname: '/', }, infraOps: { - pathname: '/app/infra' + pathname: '/app/infra', }, canvas: { pathname: '/app/canvas', hash: '/', - } + }, }, // choose where esArchiver should load archives from esArchiver: { - directory: resolve(__dirname, 'es_archives') + directory: resolve(__dirname, 'es_archives'), }, // choose where screenshots should be saved screenshots: { - directory: resolve(__dirname, 'screenshots') + directory: resolve(__dirname, 'screenshots'), }, junit: { reportName: 'X-Pack Functional Tests', rootDirectory: resolve(__dirname, '../../'), - } + }, }; } diff --git a/yarn.lock b/yarn.lock index e02ffcd6afc03..61c2868673969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5929,6 +5929,11 @@ create-react-class@^15.5.2: loose-envify "^1.3.1" object-assign "^4.1.1" +create-react-context@^0.1.5: + version "0.1.6" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc" + integrity sha512-eCnYYEUEc5i32LHwpE/W7NlddOB9oHwsPaWtWzYtflNkkwa3IfindIcoXdVWs12zCbwaMCavKNu84EXogVIWHw== + cronstrue@^1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.51.0.tgz#7a63153d61d940344049037628da38a60784c8e2" @@ -8900,6 +8905,11 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +fp-ts@^1.0.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.12.0.tgz#d333310e4ac104cdcb6bea47908e381bb09978e7" + integrity sha512-fWwnAgVlTsV26Ruo9nx+fxNHIm6l1puE1VJ/C0XJ3nRQJJJIgRHYw6sigB3MuNFZL1o4fpGlhwFhcbxHK0RsOA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -11095,6 +11105,13 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +io-ts@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.4.2.tgz#d3cb1ef7d7ba68d59af85d839a728aad7f4b1c28" + integrity sha512-U4uw8jjj8jYZip7zHgBj40GW0DpYdVi1i0J3anezp2ytYHDg7+cKc7iIFlIyCh+NLwMxzwu6OQ/b9S61KUjPGg== + dependencies: + fp-ts "^1.0.0" + ip-regex@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" @@ -20767,6 +20784,13 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unstated@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/unstated/-/unstated-2.1.1.tgz#36b124dfb2e7a12d39d0bb9c46dfb6e51276e3a2" + integrity sha512-fORlTWMZxq7NuMJDxyIrrYIZKN7wEWYQ9SiaJfIRcSpsowr6Ph/JIfK2tgtXLW614JfPG/t5q9eEIhXRCf55xg== + dependencies: + create-react-context "^0.1.5" + untildify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0"