Beats Management (#23819) (#24185)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* [Beats Management] Move to Ingest UI arch and initial TS effort (#20039)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* Ported over base types and arch structure

* move management of installIndexTemplate into the framework adapter

* ts-lint fix

* tslint fixes

* more ts tweaks

* fix paths

* added several working endpoints

* add more routes and bug fixes

* fix linting

* fix type remove CRUFT

* remove more cruft

* remove more CRUFT

* added comments, change plurality

* add tsconfig file

* add extends path

* fixed typo

* serveral PR review fixes

* fixed lodash type version

* “fix” types by applying a lot of any

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* fix auth

* updated lock file

* [Beats Management] add get beat endpoint (#20603)

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* Moved critical path code from route, to more easeley tested domain

* fix auth

* remove beat verification, added get beat endpoint to return configs

* fix type

* update createGetBeatConfigurationRoute URL

* rename method

* update to match PR #20566

* updated lock file

* fix bad merge

* update TSLinting

* fix bad rebase

* [Beats Management] [WIP] Create public resources for management plugin (#20864)

* Init plugin public resources.

* rename beats to beats_management

* rendering react now

* Beats/initial ui (#20994)

* initial layout and main nav

* modal UI and pattern for UI established

* fix path

* wire up in-memroy adapters

* tweak adapters

* add getAll method to tags adapter (#21287)

* Beats/real adapters (#21481)

* add initial real adapters, and nulled data where we need endpoints

* UI adapters and needed endpoints added (though not tested)

* prep for route tests and some cleanup

* move files

* [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182)

* Add BeatsTable and control bar components.

* Clean yarn.lock.

* Move raw numbers/strings to constants. Remove obsolete state/props.

* Update/add tests.

* Change prop name from "items" to "beats".

* Rename some variables.

* Move search bar filter definitions to table render.

* Update table to support assignment options.

* Update action control position.

* Refactor split render function into custom components.

* Beats/basic use cases (#21660)

* tweak adapter responses / types. re-add enroll ui

* routes enabled, enroll now pings the server

* full enrollment path now working

* improved pinging for beat enrollment

* fix location of history call

* reload beats list on beat enrollment completion

* [Beats Management] Add Tags List (#21274)

* Add BeatsTable and control bar components.

* Clean yarn.lock.

* Move raw numbers/strings to constants. Remove obsolete state/props.

* Update/add tests.

* Change prop name from "items" to "beats".

* Add TagsTable component and associated search/action bar.

* Rename some variables.

* Add constant after forgetting to save file.

* Fix design mistake in table component.

* Disable delete button when no tags selected.

* Export tags table from index.ts.

* Move search bar filter definitions to table render.

* Update table to support assignment options.

* Update action control position.

* Refactor split render function into custom components.

* Add assignment options to Tags List.

* Remove obsolete code.

* Move tooltips for tag icons to top position.

* Beats/update (#21702)

* [ML] Fixing issue with historical job audit messages (#21718)

* Add proper aria-label for close inspector (#21719)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* [Beats Management] Move to Ingest UI arch and initial TS effort (#20039)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* Ported over base types and arch structure

* move management of installIndexTemplate into the framework adapter

* ts-lint fix

* tslint fixes

* more ts tweaks

* fix paths

* added several working endpoints

* add more routes and bug fixes

* fix linting

* fix type remove CRUFT

* remove more cruft

* remove more CRUFT

* added comments, change plurality

* add tsconfig file

* add extends path

* fixed typo

* serveral PR review fixes

* fixed lodash type version

* “fix” types by applying a lot of any

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* fix auth

* updated lock file

* [Beats Management] add get beat endpoint (#20603)

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* Moved critical path code from route, to more easeley tested domain

* fix auth

* remove beat verification, added get beat endpoint to return configs

* fix type

* update createGetBeatConfigurationRoute URL

* rename method

* update to match PR #20566

* updated lock file

* fix bad merge

* update TSLinting

* fix bad rebase

* [Beats Management] [WIP] Create public resources for management plugin (#20864)

* Init plugin public resources.

* rename beats to beats_management

* rendering react now

* Beats/initial ui (#20994)

* initial layout and main nav

* modal UI and pattern for UI established

* fix path

* wire up in-memroy adapters

* tweak adapters

* add getAll method to tags adapter (#21287)

* Beats/real adapters (#21481)

* add initial real adapters, and nulled data where we need endpoints

* UI adapters and needed endpoints added (though not tested)

* prep for route tests and some cleanup

* move files

* [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182)

* Add BeatsTable and control bar components.

* Clean yarn.lock.

* Move raw numbers/strings to constants. Remove obsolete state/props.

* Update/add tests.

* Change prop name from "items" to "beats".

* Rename some variables.

* Move search bar filter definitions to table render.

* Update table to support assignment options.

* Update action control position.

* Refactor split render function into custom components.

* Beats/basic use cases (#21660)

* tweak adapter responses / types. re-add enroll ui

* routes enabled, enroll now pings the server

* full enrollment path now working

* improved pinging for beat enrollment

* fix location of history call

* reload beats list on beat enrollment completion

* add update on client side, expand update on server to allow for partial data, and user auth

* remove double beat lookup

* fix tests

* only return active beats

* disenroll now working

* fig getAll query

* re-enrolling a beat will now work

* fix types

* fix types

* update deps

* update kibana API for version

* [Beats CM] Manage Tags (#21776)

* [ML] Fixing issue with historical job audit messages (#21718)

* Add proper aria-label for close inspector (#21719)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* [Beats Management] Move to Ingest UI arch and initial TS effort (#20039)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Rename "determine" to "find"

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Updating ES archive

* Renaming

* Use destructuring

* Moving start of script to own line to increase readability

* Using destructuring

* [Beats Management] APIs: Remove tag(s) from beat(s) (#19440)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* Starting to implement POST /api/beats/beats_tags API

* Changing API

* Updating tests for changes to API

* Renaming

* Use destructuring

* Using crypto.timingSafeEqual() for comparing auth tokens

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Implementing `POST /api/beats/agents_tags/removals` API

* Updating ES archive

* Use destructuring

* Moving start of script to own line to increase readability

* Nothing to remove if there are no existing tags!

* Updating tests to match changes in bulk update painless script

* Use destructuring

* Ported over base types and arch structure

* move management of installIndexTemplate into the framework adapter

* ts-lint fix

* tslint fixes

* more ts tweaks

* fix paths

* added several working endpoints

* add more routes and bug fixes

* fix linting

* fix type remove CRUFT

* remove more cruft

* remove more CRUFT

* added comments, change plurality

* add tsconfig file

* add extends path

* fixed typo

* serveral PR review fixes

* fixed lodash type version

* “fix” types by applying a lot of any

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* fix auth

* updated lock file

* [Beats Management] add get beat endpoint (#20603)

* [Beats Management] Move tokens to use JWT, add more complete test suite (#20317)

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* fix broken test, this is beats CM not logstash 😊

* added readme

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* inital effort to move to JWT and added jest based tests on libs

* assign beats tests all passing

* token tests now pass

* add more tests

* all tests now green

* move enrollment token back to a hash

* remove un-needed comment

* alias lodash get to avoid confusion

* isolated hash creation

* Add initial efforts for backend framework adapter testing

* move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES

* re-typed

* renamed types to match pattern

* aditional renames

* adapter tests should always just use adapterSetup();

* database now uses InternalRequest

* corrected spelling of framework

* fix typings

* remove CRUFT

* RequestOrInternal

* Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible

* fix tests, add test, removed extra comment

* Moved critical path code from route, to more easeley tested domain

* fix auth

* remove beat verification, added get beat endpoint to return configs

* fix type

* update createGetBeatConfigurationRoute URL

* rename method

* update to match PR #20566

* updated lock file

* fix bad merge

* update TSLinting

* fix bad rebase

* [Beats Management] [WIP] Create public resources for management plugin (#20864)

* Init plugin public resources.

* rename beats to beats_management

* rendering react now

* Beats/initial ui (#20994)

* initial layout and main nav

* modal UI and pattern for UI established

* fix path

* wire up in-memroy adapters

* tweak adapters

* add getAll method to tags adapter (#21287)

* Beats/real adapters (#21481)

* add initial real adapters, and nulled data where we need endpoints

* UI adapters and needed endpoints added (though not tested)

* prep for route tests and some cleanup

* move files

* [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182)

* Add BeatsTable and control bar components.

* Clean yarn.lock.

* Move raw numbers/strings to constants. Remove obsolete state/props.

* Update/add tests.

* Change prop name from "items" to "beats".

* Rename some variables.

* Move search bar filter definitions to table render.

* Update table to support assignment options.

* Update action control position.

* Refactor split render function into custom components.

* Beats/basic use cases (#21660)

* tweak adapter responses / types. re-add enroll ui

* routes enabled, enroll now pings the server

* full enrollment path now working

* improved pinging for beat enrollment

* fix location of history call

* reload beats list on beat enrollment completion

* add update on client side, expand update on server to allow for partial data, and user auth

* remove double beat lookup

* fix tests

* only return active beats

* disenroll now working

* fig getAll query

* re-enrolling a beat will now work

* fix types

* Add create tags view.

* fix types

* update deps

* update kibana API for version

* Added component/config interface for editing/creating tags. Added separate pages for create/edit tags.

* Fixup.

* Beats/beat tags workflow (#21923)

* [Beats Management] Move to Ingest UI arch and initial TS effort (#20039)

* [Beats Management] Initial scaffolding for plugin (#18977)

* Initial scaffolding for Beats plugin

* Removing bits not (yet) necessary in initial scaffolding

* [Beats Management] Install Beats index template on plugin init (#19072)

* Install Beats index template on plugin init

* Adding missing files

* [Beats Management] APIs: Create enrollment tokens (#19018)

* WIP checkin

* Register API routes

* Fixing typo in index name

* Adding TODOs

* Removing commented out license checking code that isn't yet implemented

* Remove unnecessary async/await

* Don't return until indices have been refreshed

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Adding TODO

* Fixing variable name

* Using a single index

* Adding expiration date field

* Adding test for expiration date field

* Ignore non-existent index

* Fixing logic in test

* Creating constant for default enrollment tokens TTL value

* Updating test

* Fixing name of test file (#19100)

* [Beats Management] APIs: Enroll beat (#19056)

* WIP checkin

* Add API integration test

* Converting to Jest test

* Create API for enrolling a beat

* Handle invalid or expired enrollment tokens

* Use create instead of index to prevent same beat from being enrolled twice

* Adding unit test for duplicate beat enrollment

* Do not persist enrollment token with beat once token has been checked and used

* Fix datatype of host_ip field

* Make Kibana API guess host IP instead of requiring it in payload

* Fixing error introduced in rebase conflict resolution

* [Beats Management] APIs: List beats (#19086)

* WIP checkin

* Add API integration test

* Converting to Jest test

* WIP checkin

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Updating mapping

* [Beats Management] APIs: Verify beats (#19103)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Fleshing out remaining tests

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Moving TODO comment to right file

* Rename determine* helper functions to find*

* Fixing assertions (#19194)

* [Beats Management] APIs: Update beat (#19148)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Add API tests

* Update template to allow version field for beat

* Implement PUT /api/beats/agent/{beat ID} API

* Make enroll beat code consistent with update beat code

* Fixing minor typo in TODO comment

* Allow version in request payload

* Make sure beat is not updated in ES in error scenarios

* Adding version as required field in Enroll Beat API payload

* Using destructuring

* Fixing rename that was accidentally reversed in conflict fixing

* [Beats Management] APIs: take auth tokens via headers (#19210)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Make "Enroll Beat" API take enrollment token via header instead of request body

* Make "Update Beat" API take access token via header instead of request body

* [Beats Management] APIs: Create configuration block (#19270)

* WIP checkin

* WIP checkin

* Add API integration test

* Converting to Jest test

* Fixing API for default case + adding test for it

* Fixing copy pasta typos

* Fixing variable name

* Using a single index

* Implementing GET /api/beats/agents API

* Creating POST /api/beats/agents/verify API

* Refactoring: extracting out helper functions

* Expanding TODO note so I won't forget :)

* Fixing file name

* Updating mapping

* Fixing minor typo in TODO comment

* Implementing POST /api/beats/configuration_blocks API

* Removing unnecessary escaping

* Fleshing out types + adding validation for them

* Making output singular (was outputs)

* Removing metricbeat.inputs

* Revert implementation of `POST /api/beats/configuration_blocks` API (#19340)

This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks).

* [Beats Management] APIs: Create or update tag (#19342)

* Updating mappings

* Implementing PUT /api/beats/tag/{tag} API

* [Beats Management] Prevent timing attacks when checking auth tokens (#19363)

* Using crypto.timingSafeEqual() for comparing auth tokens

* Prevent subtler timing attack in token comparison function

* Introduce random delay after we try to find token in ES to mitigate timing attack

* Remove random delay

* [Beats Management] APIs: Assign tag(s) to beat(s) (#19431…
This commit is contained in:
Matt Apperson 2018-10-18 08:51:36 -04:00 committed by GitHub
parent 44c89d9360
commit d8f65387af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
278 changed files with 13611 additions and 83 deletions

View file

@ -77,6 +77,9 @@
"type": "git",
"url": "https://github.com/elastic/kibana.git"
},
"resolutions": {
"**/@types/node": "8.10.21"
},
"dependencies": {
"@elastic/eui": "4.4.1",
"@elastic/filesaver": "1.1.2",
@ -234,9 +237,9 @@
"@kbn/eslint-plugin-license-header": "link:packages/kbn-eslint-plugin-license-header",
"@kbn/plugin-generator": "link:packages/kbn-plugin-generator",
"@kbn/test": "link:packages/kbn-test",
"@octokit/rest": "^15.10.0",
"@types/angular": "^1.6.50",
"@types/angular-mocks": "^1.7.0",
"@octokit/rest": "^15.10.0",
"@types/babel-core": "^6.25.5",
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",

View file

@ -16,6 +16,7 @@ import { watcher } from './plugins/watcher';
import { grokdebugger } from './plugins/grokdebugger';
import { dashboardMode } from './plugins/dashboard_mode';
import { logstash } from './plugins/logstash';
import { beats } from './plugins/beats_management';
import { apm } from './plugins/apm';
import { licenseManagement } from './plugins/license_management';
import { cloud } from './plugins/cloud';
@ -42,6 +43,7 @@ module.exports = function (kibana) {
grokdebugger(kibana),
dashboardMode(kibana),
logstash(kibana),
beats(kibana),
apm(kibana),
canvas(kibana),
licenseManagement(kibana),

View file

@ -20,6 +20,9 @@
"intermediateBuildDirectory": "build/plugin/kibana/x-pack"
}
},
"resolutions": {
"**/@types/node": "8.10.21"
},
"devDependencies": {
"@kbn/dev-utils": "link:../packages/kbn-dev-utils",
"@kbn/es": "link:../packages/kbn-es",
@ -38,6 +41,7 @@
"@types/history": "^4.6.2",
"@types/jest": "^23.3.1",
"@types/joi": "^10.4.4",
"@types/jsonwebtoken": "^7.2.7",
"@types/lodash": "^3.10.1",
"@types/mocha": "^5.2.5",
"@types/pngjs": "^3.3.1",
@ -46,7 +50,7 @@
"@types/react-datepicker": "^1.1.5",
"@types/react-dom": "^16.0.5",
"@types/react-redux": "^6.0.6",
"@types/react-router-dom": "^4.2.6",
"@types/react-router-dom": "4.2.6",
"@types/reduce-reducers": "^0.1.3",
"@types/sinon": "^5.0.1",
"@types/supertest": "^2.0.5",
@ -157,6 +161,7 @@
"extract-zip": "1.5.0",
"file-saver": "^1.3.8",
"font-awesome": "4.4.0",
"formsy-react": "^1.1.5",
"get-port": "2.1.0",
"getos": "^3.1.0",
"glob": "6.0.4",
@ -173,6 +178,7 @@
"isomorphic-fetch": "2.2.1",
"joi": "6.10.1",
"jquery": "^3.3.1",
"jsonwebtoken": "^8.3.0",
"jstimezonedetect": "1.0.5",
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
"lodash.clone": "^4.5.0",

View file

@ -0,0 +1,15 @@
/*
* 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 enum ConfigurationBlockTypes {
FilebeatInputs = 'filebeat.inputs',
FilebeatModules = 'filebeat.modules',
MetricbeatModules = 'metricbeat.modules',
Output = 'output',
Processors = 'processors',
}
export const UNIQUENESS_ENFORCING_TYPES = [ConfigurationBlockTypes.Output];

View file

@ -0,0 +1,11 @@
/*
* 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 { 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 { TABLE_CONFIG } from './table';

View file

@ -0,0 +1,9 @@
/*
* 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 INDEX_NAMES = {
BEATS: '.management-beats',
};

View file

@ -0,0 +1,9 @@
/*
* 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 PLUGIN = {
ID: 'beats_management',
};

View file

@ -0,0 +1,12 @@
/*
* 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 TABLE_CONFIG = {
INITIAL_ROW_SIZE: 5,
PAGE_SIZE_OPTIONS: [3, 5, 10, 20],
TRUNCATE_TAG_LENGTH: 33,
TRUNCATE_TAG_LENGTH_SMALL: 20,
};

View file

@ -0,0 +1,87 @@
/*
* 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 { ConfigurationBlockTypes } from './constants';
export enum FilebeatModuleName {
system = 'system',
apache2 = 'apache2',
nginx = 'nginx',
mongodb = 'mongodb',
elasticsearch = 'elasticsearch',
}
export enum MetricbeatModuleName {
system = 'system',
apache2 = 'apache2',
nginx = 'nginx',
mongodb = 'mongodb',
elasticsearch = 'elasticsearch',
}
export enum OutputType {
elasticsearch = 'elasticsearch',
logstash = 'logstash',
kafka = 'kafka',
console = 'console',
}
export interface FilebeatInputsConfig {
paths: string[];
other: string;
}
export interface FilebeatModuleConfig {
module: FilebeatModuleName;
other: string;
}
export interface MetricbeatModuleConfig {
module: MetricbeatModuleName;
hosts?: string[];
period: string;
other: string;
}
export type ConfigContent = FilebeatInputsConfig | FilebeatModuleConfig | MetricbeatModuleConfig;
export interface ConfigurationBlock {
type: ConfigurationBlockTypes;
description: string;
configs: ConfigContent[];
}
export interface ReturnedConfigurationBlock
extends Pick<ConfigurationBlock, Exclude<keyof ConfigurationBlock, 'configs'>> {
config: ConfigContent;
}
export interface CMBeat {
id: string;
enrollment_token: string;
active: boolean;
access_token: string;
verified_on?: string;
type: string;
version?: string;
host_ip: string;
host_name: string;
ephemeral_id?: string;
last_updated?: string;
event_rate?: string;
local_configuration_yml?: string;
tags?: string[];
central_configuration_yml?: string;
metadata?: {};
name?: string;
}
export interface CMPopulatedBeat extends CMBeat {
full_tags: BeatTag[];
}
export interface BeatTag {
id: string;
configuration_blocks: ConfigurationBlock[];
color?: string;
last_updated: Date;
}

View file

@ -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 Joi from 'joi';
import { resolve } from 'path';
import { PLUGIN } from './common/constants';
import { initServerWithKibana } from './server/kibana.index';
const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes
export const config = Joi.object({
enabled: Joi.boolean().default(true),
encryptionKey: Joi.string(),
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({
id: PLUGIN.ID,
require: ['kibana', 'elasticsearch', 'xpack_main'],
publicDir: resolve(__dirname, 'public'),
uiExports: {
managementSections: ['plugins/beats_management'],
},
config: () => config,
configPrefix,
init(server: any) {
initServerWithKibana(server);
},
});
}

View file

@ -0,0 +1,14 @@
/*
* 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 type FlatObject<T> = { [Key in keyof T]: string };
export interface AppURLState {
beatsKBar?: string;
tagsKBar?: string;
enrollmentToken?: string;
createdTag?: string;
}

View file

@ -0,0 +1,290 @@
/*
* 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 {
EuiFieldSearch,
EuiFieldSearchProps,
EuiOutsideClickDetector,
EuiPanel,
} from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
// @ts-ignore
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { composeStateUpdaters } from '../../utils/typed_react';
import { SuggestionItem } from './suggestion_item';
interface AutocompleteFieldProps {
isLoadingSuggestions: boolean;
isValid: boolean;
loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void;
onSubmit?: (value: string) => void;
onChange?: (value: string) => void;
placeholder?: string;
suggestions: AutocompleteSuggestion[];
value: string;
}
interface AutocompleteFieldState {
areSuggestionsVisible: boolean;
selectedIndex: number | null;
}
export class AutocompleteField extends React.Component<
AutocompleteFieldProps,
AutocompleteFieldState
> {
public readonly state: AutocompleteFieldState = {
areSuggestionsVisible: false,
selectedIndex: null,
};
private inputElement: HTMLInputElement | null = null;
public render() {
const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props;
const { areSuggestionsVisible, selectedIndex } = this.state;
return (
<EuiOutsideClickDetector onOutsideClick={this.hideSuggestions}>
<AutocompleteContainer>
<FixedEuiFieldSearch
fullWidth
inputRef={this.handleChangeInputRef}
isLoading={isLoadingSuggestions}
isInvalid={!isValid}
onChange={this.handleChange}
onFocus={this.showSuggestions}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onSearch={this.submit}
placeholder={placeholder}
value={value}
/>
{areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? (
<SuggestionsPanel>
{suggestions.map((suggestion, suggestionIndex) => (
<SuggestionItem
key={suggestion.text}
suggestion={suggestion}
isSelected={suggestionIndex === selectedIndex}
onMouseEnter={this.selectSuggestionAt(suggestionIndex)}
onClick={this.applySuggestionAt(suggestionIndex)}
/>
))}
</SuggestionsPanel>
) : null}
</AutocompleteContainer>
</EuiOutsideClickDetector>
);
}
public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) {
const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions;
const hasNewValue = prevProps.value !== this.props.value;
if (hasNewValue) {
this.updateSuggestions();
}
if (hasNewSuggestions) {
this.showSuggestions();
}
}
private handleChangeInputRef = (element: HTMLInputElement | null) => {
this.inputElement = element;
};
private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
this.changeValue(evt.currentTarget.value);
};
private handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
const { suggestions } = this.props;
switch (evt.key) {
case 'ArrowUp':
evt.preventDefault();
if (suggestions.length > 0) {
this.setState(
composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected)
);
}
break;
case 'ArrowDown':
evt.preventDefault();
if (suggestions.length > 0) {
this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected));
} else {
this.updateSuggestions();
}
break;
case 'Enter':
evt.preventDefault();
if (this.state.selectedIndex !== null) {
this.applySelectedSuggestion();
} else {
this.submit();
}
break;
case 'Escape':
evt.preventDefault();
this.setState(withSuggestionsHidden);
break;
}
};
private handleKeyUp = (evt: React.KeyboardEvent<HTMLInputElement>) => {
switch (evt.key) {
case 'ArrowLeft':
case 'ArrowRight':
case 'Home':
case 'End':
this.updateSuggestions();
break;
}
};
private selectSuggestionAt = (index: number) => () => {
this.setState(withSuggestionAtIndexSelected(index));
};
private applySelectedSuggestion = () => {
if (this.state.selectedIndex !== null) {
this.applySuggestionAt(this.state.selectedIndex)();
}
};
private applySuggestionAt = (index: number) => () => {
const { value, suggestions } = this.props;
const selectedSuggestion = suggestions[index];
if (!selectedSuggestion) {
return;
}
const newValue =
value.substr(0, selectedSuggestion.start) +
selectedSuggestion.text +
value.substr(selectedSuggestion.end);
this.setState(withSuggestionsHidden);
this.changeValue(newValue);
this.focusInputElement();
};
private changeValue = (value: string) => {
const { onChange } = this.props;
if (onChange) {
onChange(value);
}
};
private focusInputElement = () => {
if (this.inputElement) {
this.inputElement.focus();
}
};
private showSuggestions = () => {
this.setState(withSuggestionsVisible);
};
private hideSuggestions = () => {
this.setState(withSuggestionsHidden);
};
private submit = () => {
const { isValid, onSubmit, value } = this.props;
if (isValid && onSubmit) {
onSubmit(value);
}
this.setState(withSuggestionsHidden);
};
private updateSuggestions = (value?: string) => {
const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0;
this.props.loadSuggestions(value || this.props.value, inputCursorPosition, 10);
};
}
const withPreviousSuggestionSelected = (
state: AutocompleteFieldState,
props: AutocompleteFieldProps
): AutocompleteFieldState => ({
...state,
selectedIndex:
props.suggestions.length === 0
? null
: state.selectedIndex !== null
? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length
: Math.max(props.suggestions.length - 1, 0),
});
const withNextSuggestionSelected = (
state: AutocompleteFieldState,
props: AutocompleteFieldProps
): AutocompleteFieldState => ({
...state,
selectedIndex:
props.suggestions.length === 0
? null
: state.selectedIndex !== null
? (state.selectedIndex + 1) % props.suggestions.length
: 0,
});
const withSuggestionAtIndexSelected = (suggestionIndex: number) => (
state: AutocompleteFieldState,
props: AutocompleteFieldProps
): AutocompleteFieldState => ({
...state,
selectedIndex:
props.suggestions.length === 0
? null
: suggestionIndex >= 0 && suggestionIndex < props.suggestions.length
? suggestionIndex
: 0,
});
const withSuggestionsVisible = (state: AutocompleteFieldState) => ({
...state,
areSuggestionsVisible: true,
});
const withSuggestionsHidden = (state: AutocompleteFieldState) => ({
...state,
areSuggestionsVisible: false,
selectedIndex: null,
});
const FixedEuiFieldSearch: React.SFC<
React.InputHTMLAttributes<HTMLInputElement> &
EuiFieldSearchProps & {
inputRef?: (element: HTMLInputElement | null) => void;
onSearch: (value: string) => void;
}
> = EuiFieldSearch as any;
const AutocompleteContainer = styled.div`
position: relative;
`;
const SuggestionsPanel = styled(EuiPanel).attrs({
paddingSize: 'none',
hasShadow: true,
})`
position: absolute;
width: 100%;
margin-top: 2px;
overflow: hidden;
z-index: 1000;
`;

View file

@ -0,0 +1,123 @@
/*
* 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 { EuiIcon } from '@elastic/eui';
import { tint } from 'polished';
import React from 'react';
import styled from 'styled-components';
// @ts-ignore
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
interface SuggestionItemProps {
isSelected?: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
suggestion: AutocompleteSuggestion;
}
export class SuggestionItem extends React.Component<SuggestionItemProps> {
public static defaultProps: Partial<SuggestionItemProps> = {
isSelected: false,
};
public render() {
const { isSelected, onClick, onMouseEnter, suggestion } = this.props;
return (
<SuggestionItemContainer
isSelected={isSelected}
onClick={onClick}
onMouseEnter={onMouseEnter}
>
<SuggestionItemIconField suggestionType={suggestion.type}>
<EuiIcon type={getEuiIconType(suggestion.type)} />
</SuggestionItemIconField>
<SuggestionItemTextField>{suggestion.text}</SuggestionItemTextField>
<SuggestionItemDescriptionField
dangerouslySetInnerHTML={{ __html: suggestion.description }}
/>
</SuggestionItemContainer>
);
}
}
const SuggestionItemContainer = styled.div<{
isSelected?: boolean;
}>`
display: flex;
flex-direction: row;
font-size: ${props => props.theme.eui.euiFontSizeS};
height: ${props => props.theme.eui.euiSizeXl};
white-space: nowrap;
background-color: ${props =>
props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'};
`;
const SuggestionItemField = styled.div`
align-items: center;
cursor: pointer;
display: flex;
flex-direction: row;
height: ${props => props.theme.eui.euiSizeXl};
padding: ${props => props.theme.eui.euiSizeXs};
`;
const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>`
background-color: ${props => tint(0.1, getEuiIconColor(props.theme, props.suggestionType))};
color: ${props => getEuiIconColor(props.theme, props.suggestionType)};
flex: 0 0 auto;
justify-content: center;
width: ${props => props.theme.eui.euiSizeXl};
`;
const SuggestionItemTextField = SuggestionItemField.extend`
flex: 2 0 0;
font-family: ${props => props.theme.eui.euiCodeFontFamily};
`;
const SuggestionItemDescriptionField = SuggestionItemField.extend`
flex: 3 0 0;
p {
display: inline;
span {
font-family: ${props => props.theme.eui.euiCodeFontFamily};
}
}
`;
const getEuiIconType = (suggestionType: string) => {
switch (suggestionType) {
case 'field':
return 'kqlField';
case 'value':
return 'kqlValue';
case 'recentSearch':
return 'search';
case 'conjunction':
return 'kqlSelector';
case 'operator':
return 'kqlOperand';
default:
return 'empty';
}
};
const getEuiIconColor = (theme: any, suggestionType: string): string => {
switch (suggestionType) {
case 'field':
return theme.eui.euiColorVis7;
case 'value':
return theme.eui.euiColorVis0;
case 'operator':
return theme.eui.euiColorVis1;
case 'conjunction':
return theme.eui.euiColorVis2;
case 'recentSearch':
default:
return theme.eui.euiColorMediumShade;
}
};

View file

@ -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.
*/
// @ts-ignore
import { EuiBasicTable, EuiLink } from '@elastic/eui';
import React from 'react';
import { ConfigurationBlock } from '../../common/domain_types';
import { supportedConfigs } from '../config_schemas';
interface ComponentProps {
configs: ConfigurationBlock[];
onConfigClick: (action: 'edit' | 'delete', config: ConfigurationBlock) => any;
}
export const ConfigList: React.SFC<ComponentProps> = props => (
<EuiBasicTable
items={props.configs || []}
columns={[
{
field: 'type',
name: 'Type',
truncateText: false,
render: (value: string, config: ConfigurationBlock) => {
const type = supportedConfigs.find((sc: any) => sc.value === config.type);
return (
<EuiLink onClick={() => props.onConfigClick('edit', config)}>
{type ? type.text : config.type}
</EuiLink>
);
},
},
{
field: 'module',
name: 'Module',
truncateText: false,
render: (value: string) => {
return value || 'N/A';
},
},
{
field: 'description',
name: 'Description',
},
{
name: 'Actions',
actions: [
{
name: 'Remove',
description: 'Remove this config from tag',
type: 'icon',
icon: 'trash',
onClick: (item: ConfigurationBlock) => props.onConfigClick('delete', item),
},
],
},
]}
/>
);

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiLink } from '@elastic/eui';
import { Link, withRouter } from 'react-router-dom';
export function ConnectedLinkComponent({
location,
path,
query,
disabled,
...props
}: {
location: any;
path: string;
disabled: boolean;
query: any;
[key: string]: any;
}) {
if (disabled) {
return <EuiLink aria-disabled="true" {...props} />;
}
// Shorthand for pathname
const pathname = path || _.get(props.to, 'pathname') || location.pathname;
return (
<Link
{...props}
to={{ ...location, ...props.to, pathname, query }}
className={`euiLink euiLink--primary ${props.className || ''}`}
/>
);
}
export const ConnectedLink = withRouter<any>(ConnectedLinkComponent);

View file

@ -0,0 +1,113 @@
/*
* 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.
*/
// @ts-ignore
import { CommonProps, EuiCodeEditor, EuiCodeEditorProps, EuiFormRow } from '@elastic/eui';
// @ts-ignore
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
interface ComponentProps extends FormsyInputProps, CommonProps, EuiCodeEditorProps {
instantValidation: boolean;
label: string;
isReadOnly: boolean;
mode: 'javascript' | 'yaml';
errorText: string;
fullWidth: boolean;
helpText: React.ReactElement<any>;
compressed: boolean;
onChange(value: string): void;
onBlur(): void;
}
interface ComponentState {
allowError: boolean;
}
class CodeEditor extends Component<
InputHTMLAttributes<HTMLTextAreaElement> & ComponentProps,
ComponentState
> {
public static defaultProps = {
passRequiredToField: true,
};
public state = { allowError: false };
public componentDidMount() {
const { defaultValue, setValue } = this.props;
setValue(defaultValue || '');
}
public componentWillReceiveProps(nextProps: ComponentProps) {
if (nextProps.isFormSubmitted()) {
this.showError();
}
}
public handleChange = (value: string) => {
this.props.setValue(value);
if (this.props.onChange) {
this.props.onChange(value);
}
if (this.props.instantValidation) {
this.showError();
}
};
public handleBlur = () => {
this.showError();
if (this.props.onBlur) {
this.props.onBlur();
}
};
public showError = () => this.setState({ allowError: true });
public render() {
const {
id,
label,
isReadOnly,
isValid,
getValue,
isPristine,
getErrorMessage,
mode,
fullWidth,
className,
helpText,
} = this.props;
const { allowError } = this.state;
const error = !isPristine() && !isValid() && allowError;
return (
<EuiFormRow
id={id}
label={label}
helpText={helpText}
isInvalid={error}
error={error ? getErrorMessage() : []}
>
<EuiCodeEditor
id={id}
name={name}
mode={mode}
theme="github"
value={getValue() || ''}
isReadOnly={isReadOnly || false}
isInvalid={error}
onChange={this.handleChange}
onBlur={this.handleBlur}
width={fullWidth ? '100%' : undefined}
className={className}
/>
</EuiFormRow>
);
}
}
export const FormsyEuiCodeEditor = withFormsy(CodeEditor);

View file

@ -0,0 +1,11 @@
/*
* 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 { FormsyEuiCodeEditor } from './code_editor';
export { FormsyEuiFieldText } from './input';
export { FormsyEuiPasswordText } from './password_input';
export { FormsyEuiMultiFieldText } from './multi_input';
export { FormsyEuiSelect } from './select';

View file

@ -0,0 +1,111 @@
/*
* 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 { CommonProps, EuiFieldText, EuiFieldTextProps, EuiFormRow } from '@elastic/eui';
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
interface ComponentProps extends FormsyInputProps, CommonProps, EuiFieldTextProps {
instantValidation?: boolean;
label: string;
errorText: string;
fullWidth: boolean;
helpText: React.ReactElement<any>;
compressed: boolean;
onChange?(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
onBlur?(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
}
interface ComponentState {
allowError: boolean;
}
class FieldText extends Component<
InputHTMLAttributes<HTMLInputElement> & ComponentProps,
ComponentState
> {
public static defaultProps = {
passRequiredToField: true,
};
public state = { allowError: false };
public componentDidMount() {
const { defaultValue, setValue } = this.props;
if (defaultValue) {
setValue(defaultValue);
}
}
public componentWillReceiveProps(nextProps: ComponentProps) {
if (nextProps.isFormSubmitted()) {
this.showError();
}
}
public handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
this.props.setValue(value);
if (this.props.onChange) {
this.props.onChange(e, e.currentTarget.value);
}
if (this.props.instantValidation) {
this.showError();
}
};
public handleBlur = (e: React.ChangeEvent<HTMLInputElement>) => {
this.showError();
if (this.props.onBlur) {
this.props.onBlur(e, e.currentTarget.value);
}
};
public showError = () => this.setState({ allowError: true });
public render() {
const {
id,
required,
label,
getValue,
isValid,
isPristine,
getErrorMessage,
fullWidth,
className,
disabled,
helpText,
} = this.props;
const { allowError } = this.state;
const error = !isPristine() && !isValid() && allowError;
return (
<EuiFormRow
id={id}
label={label}
helpText={helpText}
isInvalid={!disabled && error}
error={!disabled && error ? getErrorMessage() : []}
>
<EuiFieldText
id={id}
name={name}
value={getValue() || ''}
isInvalid={!disabled && error}
onChange={this.handleChange}
onBlur={this.handleBlur}
fullWidth={fullWidth}
disabled={disabled}
required={required}
className={className}
/>
</EuiFormRow>
);
}
}
export const FormsyEuiFieldText = withFormsy(FieldText);

View file

@ -0,0 +1,113 @@
/*
* 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 { CommonProps, EuiFormRow, EuiTextArea, EuiTextAreaProps } from '@elastic/eui';
// @ts-ignore
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
interface ComponentProps extends FormsyInputProps, CommonProps, EuiTextAreaProps {
instantValidation: boolean;
label: string;
errorText: string;
fullWidth: boolean;
helpText: React.ReactElement<any>;
compressed: boolean;
onChange(e: React.ChangeEvent<HTMLTextAreaElement>, value: any): void;
onBlur(e: React.ChangeEvent<HTMLTextAreaElement>, value: any): void;
}
interface ComponentState {
allowError: boolean;
}
class MultiFieldText extends Component<
InputHTMLAttributes<HTMLTextAreaElement> & ComponentProps,
ComponentState
> {
public static defaultProps = {
passRequiredToField: true,
};
public state = { allowError: false };
public componentDidMount() {
const { defaultValue, setValue } = this.props;
if (defaultValue) {
setValue(defaultValue);
}
}
public componentWillReceiveProps(nextProps: ComponentProps) {
if (nextProps.isFormSubmitted()) {
this.showError();
}
}
public handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.currentTarget.value.split('\n');
this.props.setValue(value);
if (this.props.onChange) {
this.props.onChange(e, value);
}
if (this.props.instantValidation) {
this.showError();
}
};
public handleBlur = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.showError();
if (this.props.onBlur) {
this.props.onBlur(e, e.currentTarget.value);
}
};
public showError = () => this.setState({ allowError: true });
public render() {
const {
id,
required,
label,
getValue,
isValid,
isPristine,
getErrorMessage,
fullWidth,
className,
disabled,
helpText,
} = this.props;
const { allowError } = this.state;
const error = !isPristine() && !isValid() && allowError;
return (
<EuiFormRow
id={id}
label={label}
helpText={helpText}
isInvalid={!disabled && error}
error={!disabled && error ? getErrorMessage() : []}
>
<EuiTextArea
id={id}
name={name}
value={getValue() ? getValue().join('\n') : ''}
isInvalid={!disabled && error}
onChange={this.handleChange}
onBlur={this.handleBlur}
fullWidth={fullWidth}
disabled={disabled}
required={required}
className={className}
/>
</EuiFormRow>
);
}
}
export const FormsyEuiMultiFieldText = withFormsy(MultiFieldText);

View file

@ -0,0 +1,110 @@
/*
* 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.
*/
// @ts-ignore currently no definition for EuiFieldPassword
import { CommonProps, EuiFieldPassword, EuiFieldPasswordProps, EuiFormRow } from '@elastic/eui';
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
interface ComponentProps extends FormsyInputProps, CommonProps, EuiFieldPasswordProps {
instantValidation?: boolean;
label: string;
errorText: string;
fullWidth: boolean;
helpText: React.ReactElement<any>;
compressed: boolean;
onChange?(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
onBlur?(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
}
interface ComponentState {
allowError: boolean;
}
class FieldPassword extends Component<
InputHTMLAttributes<HTMLInputElement> & ComponentProps,
ComponentState
> {
constructor(props: any) {
super(props);
this.state = {
allowError: false,
};
}
public componentDidMount() {
const { defaultValue, setValue } = this.props;
if (defaultValue) {
setValue(defaultValue);
}
}
public handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
this.props.setValue(value);
if (this.props.onChange) {
this.props.onChange(e, value);
}
if (this.props.instantValidation) {
this.showError();
}
};
public handleBlur = (e: React.ChangeEvent<HTMLInputElement>) => {
this.showError();
if (this.props.onBlur) {
this.props.onBlur(e, e.currentTarget.value);
}
};
public showError = () => this.setState({ allowError: true });
public render() {
const {
id,
required,
label,
getValue,
isValid,
isPristine,
getErrorMessage,
fullWidth,
className,
disabled,
helpText,
onBlur,
} = this.props;
const { allowError } = this.state;
const error = !isPristine() && !isValid() && allowError;
return (
<EuiFormRow
id={id}
label={label}
helpText={helpText}
isInvalid={!disabled && error}
error={!disabled && error ? getErrorMessage() : []}
>
<EuiFieldPassword
id={id}
name={name}
value={getValue() || ''}
isInvalid={!disabled && error}
onChange={this.handleChange}
onBlur={onBlur}
fullWidth={fullWidth}
disabled={disabled}
required={required}
className={className}
/>
</EuiFormRow>
);
}
}
export const FormsyEuiPasswordText = withFormsy(FieldPassword);

View file

@ -0,0 +1,124 @@
/*
* 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 {
CommonProps,
EuiFormRow,
// @ts-ignore
EuiSelect,
} from '@elastic/eui';
// @ts-ignore
import { FormsyInputProps, withFormsy } from 'formsy-react';
import React, { Component, InputHTMLAttributes } from 'react';
const FixedSelect = EuiSelect as React.SFC<any>;
interface ComponentProps extends FormsyInputProps, CommonProps {
instantValidation: boolean;
options: Array<{ value: string; text: string }>;
label: string;
errorText: string;
fullWidth: boolean;
helpText: React.ReactElement<any>;
compressed: boolean;
onChange(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
onBlur(e: React.ChangeEvent<HTMLInputElement>, value: any): void;
}
interface ComponentState {
allowError: boolean;
}
class FieldSelect extends Component<
InputHTMLAttributes<HTMLInputElement> & ComponentProps,
ComponentState
> {
public static defaultProps = {
passRequiredToField: true,
};
public state = { allowError: false };
public componentDidMount() {
const { defaultValue, setValue } = this.props;
if (defaultValue) {
setValue(defaultValue);
}
}
public componentWillReceiveProps(nextProps: ComponentProps) {
if (nextProps.isFormSubmitted()) {
this.showError();
}
}
public handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
this.props.setValue(value);
if (this.props.onChange) {
this.props.onChange(e, e.currentTarget.value);
}
if (this.props.instantValidation) {
this.showError();
}
};
public handleBlur = (e: React.ChangeEvent<HTMLInputElement>) => {
this.showError();
if (this.props.onBlur) {
this.props.onBlur(e, e.currentTarget.value);
}
};
public showError = () => this.setState({ allowError: true });
public render() {
const {
id,
required,
label,
options,
getValue,
isValid,
isPristine,
getErrorMessage,
fullWidth,
className,
disabled,
helpText,
} = this.props;
const { allowError } = this.state;
const error = !isPristine() && !isValid() && allowError;
return (
<EuiFormRow
id={id}
label={label}
helpText={helpText}
isInvalid={!disabled && error}
error={!disabled && error ? getErrorMessage() : []}
>
<FixedSelect
id={id}
name={name}
value={getValue() || ''}
options={options}
isInvalid={!disabled && error}
onChange={this.handleChange}
onBlur={this.handleBlur}
fullWidth={fullWidth}
disabled={disabled}
required={required}
className={className}
/>
</EuiFormRow>
);
}
}
export const FormsyEuiSelect = withFormsy(FieldSelect);

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
EuiBreadcrumbDefinition,
EuiHeader,
EuiHeaderBreadcrumbs,
EuiHeaderSection,
} from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
interface HeaderProps {
breadcrumbs?: EuiBreadcrumbDefinition[];
}
export class Header extends React.PureComponent<HeaderProps> {
public render() {
const { breadcrumbs = [] } = this.props;
return (
<HeaderWrapper>
<EuiHeaderSection>
<EuiHeaderBreadcrumbs breadcrumbs={[...breadcrumbs]} />
</EuiHeaderSection>
</HeaderWrapper>
);
}
}
const HeaderWrapper = styled(EuiHeader)`
height: 29px;
`;

View file

@ -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 from 'react';
import { withRouter } from 'react-router-dom';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiOverlayMask,
EuiPage,
EuiPageBody,
EuiPageContent,
} from '@elastic/eui';
interface LayoutProps {
title: string;
actionSection?: React.ReactNode;
modalRender?: () => React.ReactNode;
modalClosePath?: string;
}
export const NoDataLayout: React.SFC<LayoutProps> = withRouter<any>(
({ actionSection, title, modalRender, modalClosePath, children, history }) => {
const modalContent = modalRender && modalRender();
return (
<EuiPage>
<EuiPageBody>
<EuiFlexGroup justifyContent="spaceAround" style={{ marginTop: 50 }}>
<EuiFlexItem grow={false}>
<EuiPageContent>
<EuiEmptyPrompt
iconType="logoBeats"
title={<h2>{title}</h2>}
body={children}
actions={actionSection}
/>
</EuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
{modalContent && (
<EuiOverlayMask>
<EuiModal
onClose={() => {
history.push(modalClosePath);
}}
style={{ width: '640px' }}
>
{modalContent}
</EuiModal>
</EuiOverlayMask>
)}
</EuiPage>
);
}
) as any;

View file

@ -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 from 'react';
import { withRouter } from 'react-router-dom';
import {
EuiModal,
EuiOverlayMask,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
interface PrimaryLayoutProps {
title: string;
actionSection?: React.ReactNode;
modalRender?: () => React.ReactNode;
modalClosePath?: string;
}
export const PrimaryLayout: React.SFC<PrimaryLayoutProps> = withRouter<any>(
({ actionSection, title, modalRender, modalClosePath, children, history }) => {
const modalContent = modalRender && modalRender();
return (
<EuiPage>
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle>
<h1>{title}</h1>
</EuiTitle>
</EuiPageHeaderSection>
<EuiPageHeaderSection> {actionSection}</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
{modalContent && (
<EuiOverlayMask>
<EuiModal
onClose={() => {
history.push(modalClosePath);
}}
style={{ width: '640px' }}
>
{modalContent}
</EuiModal>
</EuiOverlayMask>
)}
</EuiPage>
);
}
) as any;

View file

@ -0,0 +1,61 @@
/*
* 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 from 'react';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
// @ts-ignore
EuiStepsHorizontal,
EuiTitle,
} from '@elastic/eui';
interface LayoutProps {
title: string;
goTo: (path: string) => any;
walkthroughSteps: Array<{
id: string;
name: string;
disabled: boolean;
}>;
activePath: string;
}
export const WalkthroughLayout: React.SFC<LayoutProps> = ({
walkthroughSteps,
title,
activePath,
goTo,
children,
}) => {
const indexOfCurrent = walkthroughSteps.findIndex(step => activePath === step.id);
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiTitle>
<h1 style={{ textAlign: 'center' }}>{title}</h1>
</EuiTitle>
<br />
<br />
<EuiStepsHorizontal
steps={walkthroughSteps.map((step, i) => ({
title: step.name,
isComplete: i <= indexOfCurrent,
onClick: () => goTo(step.id),
}))}
/>
<br />
<br />
<EuiPageContentBody>{children}</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -0,0 +1,77 @@
/*
* 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, EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
import React from 'react';
import { ActionDefinition } from './table_type_configs';
interface ActionButtonProps {
actions: ActionDefinition[];
isPopoverVisible: boolean;
actionHandler(action: string, payload?: any): void;
hidePopover(): void;
showPopover(): void;
}
const Action = (props: {
action: string;
danger?: boolean;
name: string;
actionHandler(action: string, payload?: any): void;
}) => {
const { action, actionHandler, danger, name } = props;
return (
<EuiButton color={danger ? 'danger' : 'primary'} onClick={() => actionHandler(action)}>
{name}
</EuiButton>
);
};
export function ActionButton(props: ActionButtonProps) {
const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover } = props;
if (actions.length === 0) {
return null;
} else if (actions.length <= 2) {
return (
<EuiFlexGroup>
{actions.map(({ action, danger, name }) => (
<EuiFlexItem key={action} grow={false}>
<Action action={action} actionHandler={actionHandler} danger={danger} name={name} />
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}
return (
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButton iconSide="right" iconType="arrowDown" onClick={showPopover}>
Bulk Action
</EuiButton>
}
closePopover={hidePopover}
id="contextMenu"
isOpen={isPopoverVisible}
panelPaddingSize="none"
withTitle
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: 'Bulk Actions',
items: actions.map(action => ({
...action,
onClick: () => actionHandler(action.action),
})),
},
]}
/>
</EuiPopover>
);
}

View file

@ -0,0 +1,70 @@
/*
* 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 { AssignmentActionType } from './table';
export enum AssignmentComponentType {
Action,
Popover,
SelectionCount,
TagBadgeList,
}
export interface AssignmentControlSchema {
name: string;
type: AssignmentComponentType;
danger?: boolean;
action?: AssignmentActionType;
showWarning?: boolean;
warningHeading?: string;
warningMessage?: string;
lazyLoad?: boolean;
children?: AssignmentControlSchema[];
grow?: boolean;
}
export const beatsListAssignmentOptions: AssignmentControlSchema[] = [
{
type: AssignmentComponentType.Action,
grow: false,
name: 'Disenroll selected',
showWarning: true,
warningHeading: 'Disenroll beats',
warningMessage: 'This will disenroll the selected beat(s) from centralized management',
action: AssignmentActionType.Delete,
danger: true,
},
{
type: AssignmentComponentType.Popover,
name: 'Set tags',
grow: false,
lazyLoad: true,
children: [
{
name: 'Assign tags',
type: AssignmentComponentType.TagBadgeList,
},
],
},
{
type: AssignmentComponentType.SelectionCount,
grow: true,
name: 'selectionCount',
},
];
export const tagConfigAssignmentOptions: AssignmentControlSchema[] = [
{
type: AssignmentComponentType.Action,
danger: true,
grow: false,
name: 'Detach beat(s)',
showWarning: true,
warningHeading: 'Detatch beats',
warningMessage: 'This will detatch the selected beat(s) from this tag.',
action: AssignmentActionType.Delete,
},
];

View file

@ -0,0 +1,66 @@
/*
* 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 } from '@elastic/eui';
import React from 'react';
import { AutocompleteField } from '../autocomplete_field/index';
import { OptionControl } from '../table_controls';
import { AssignmentOptions as AssignmentOptionsType, KueryBarProps } from './table';
interface ControlBarProps {
assignmentOptions: AssignmentOptionsType;
kueryBarProps?: KueryBarProps;
selectionCount: number;
}
export function ControlBar(props: ControlBarProps) {
const {
assignmentOptions: { actionHandler, items, schema, type },
kueryBarProps,
selectionCount,
} = props;
if (type === 'none') {
return null;
}
const showSearch = type !== 'assignment' || selectionCount === 0;
const showAssignmentOptions = type === 'assignment' && selectionCount > 0;
const showPrimaryOptions = type === 'primary' && selectionCount > 0;
return (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
{showPrimaryOptions &&
schema.map(def => (
<EuiFlexItem key={def.name} grow={def.grow}>
<OptionControl
schema={def}
selectionCount={selectionCount}
actionHandler={actionHandler}
items={items}
/>
</EuiFlexItem>
))}
{showSearch &&
kueryBarProps && (
<EuiFlexItem>
<AutocompleteField {...kueryBarProps} placeholder="Filter results" />
</EuiFlexItem>
)}
{showAssignmentOptions &&
schema.map(def => (
<EuiFlexItem key={def.name} grow={def.grow}>
<OptionControl
schema={def}
selectionCount={selectionCount}
actionHandler={actionHandler}
items={items}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,21 @@
/*
* 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 { AssignmentActionType, AssignmentOptions, KueryBarProps, Table } from './table';
export {
AssignmentComponentType,
AssignmentControlSchema,
beatsListAssignmentOptions,
tagConfigAssignmentOptions,
} from './assignment_schema';
export { ControlBar } from './controls';
export {
ActionDefinition,
BeatDetailTagsTable,
BeatsTableType,
FilterDefinition,
TagsTableType,
} from './table_type_configs';

View file

@ -0,0 +1,122 @@
/*
* 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 no typings for EuiInMemoryTable in EUI
EuiInMemoryTable,
EuiSpacer,
} from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { TABLE_CONFIG } from '../../../common/constants';
import { AssignmentControlSchema } from './assignment_schema';
import { ControlBar } from './controls';
import { TableType } from './table_type_configs';
export enum AssignmentActionType {
Add,
Assign,
Delete,
Edit,
Reload,
Search,
}
export interface AssignmentOptions {
schema: AssignmentControlSchema[];
items: any[];
type?: 'none' | 'primary' | 'assignment';
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export interface KueryBarProps {
filterQueryDraft: string;
isLoadingSuggestions: boolean;
isValid: boolean;
loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void;
onChange?: (value: string) => void;
onSubmit?: (value: string) => void;
suggestions: AutocompleteSuggestion[];
value: string;
}
interface TableProps {
assignmentOptions?: AssignmentOptions;
hideTableControls?: boolean;
kueryBarProps?: KueryBarProps;
items: any[];
type: TableType;
}
interface TableState {
selection: any[];
}
const TableContainer = styled.div`
padding: 16px;
`;
export class Table extends React.Component<TableProps, TableState> {
constructor(props: any) {
super(props);
this.state = {
selection: [],
};
}
public resetSelection = () => {
this.setSelection([]);
};
public setSelection = (selection: any[]) => {
this.setState({
selection,
});
};
public render() {
const { assignmentOptions, hideTableControls, items, kueryBarProps, type } = this.props;
const pagination = {
initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE,
pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS,
};
const selectionOptions = hideTableControls
? null
: {
onSelectionChange: this.setSelection,
selectable: () => true,
selectableMessage: () => 'Select this beat',
selection: this.state.selection,
};
return (
<TableContainer>
{!hideTableControls &&
assignmentOptions && (
<ControlBar
assignmentOptions={assignmentOptions}
kueryBarProps={kueryBarProps}
selectionCount={this.state.selection.length}
/>
)}
<EuiSpacer size="m" />
<EuiInMemoryTable
columns={type.columnDefinitions}
items={items}
itemId="id"
isSelectable={true}
pagination={pagination}
selection={selectionOptions}
sorting={true}
/>
</TableContainer>
);
}
}

View file

@ -0,0 +1,29 @@
/*
* 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 typings for EuiSearchar not included in EUI
EuiSearchBar,
} from '@elastic/eui';
import React from 'react';
import { FilterDefinition } from '../table';
import { AssignmentActionType } from './table';
interface TableSearchControlProps {
filters?: FilterDefinition[];
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export const TableSearchControl = (props: TableSearchControlProps) => {
const { actionHandler, filters } = props;
return (
<EuiSearchBar
box={{ incremental: true }}
filters={filters}
onChange={(query: any) => actionHandler(AssignmentActionType.Search, query)}
/>
);
};

View file

@ -0,0 +1,208 @@
/*
* 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 } from '@elastic/eui';
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 { TagBadge } from '../tag';
export interface ColumnDefinition {
align?: string;
field: string;
name: string;
sortable?: boolean;
width?: string;
render?(value: any, object?: any): any;
}
export interface ActionDefinition {
action: string;
danger?: boolean;
icon?: any;
name: string;
}
interface FilterOption {
value: string;
}
export interface FilterDefinition {
field: string;
name: string;
options?: FilterOption[];
type: string;
}
export interface ControlDefinitions {
actions: ActionDefinition[];
filters: FilterDefinition[];
primaryActions?: ActionDefinition[];
}
export interface TableType {
columnDefinitions: ColumnDefinition[];
controlDefinitions(items: any[]): ControlDefinitions;
}
export const BeatsTableType: TableType = {
columnDefinitions: [
{
field: 'name',
name: 'Beat name',
render: (name: string, beat: CMPopulatedBeat) => (
<ConnectedLink path={`/beat/${beat.id}`}>{name}</ConnectedLink>
),
sortable: true,
},
{
field: 'type',
name: 'Type',
sortable: true,
},
{
field: 'full_tags',
name: 'Tags',
render: (value: string, beat: CMPopulatedBeat) => (
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
{(sortBy(beat.full_tags, 'id') || []).map(tag => (
<EuiFlexItem key={tag.id} grow={false}>
<ConnectedLink path={`/tag/edit/${tag.id}`}>
<TagBadge tag={tag} />
</ConnectedLink>
</EuiFlexItem>
))}
</EuiFlexGroup>
),
sortable: false,
},
{
// TODO: update to use actual metadata field
field: 'event_rate',
name: 'Event rate',
sortable: true,
},
{
// TODO: update to use actual metadata field
field: 'full_tags',
name: 'Last config update',
render: (tags: BeatTag[]) =>
tags.length ? (
<span>
{moment(first(sortByOrder(tags, ['last_updated'], ['desc'])).last_updated).fromNow()}
</span>
) : null,
sortable: true,
},
],
controlDefinitions: (data: any[]) => ({
actions: [
{
name: 'Disenroll Selected',
action: 'delete',
danger: true,
},
],
filters: [
{
type: 'field_value_selection',
field: 'type',
name: 'Type',
options: uniq(data.map(({ type }: { type: any }) => ({ value: type })), 'value'),
},
],
}),
};
export const TagsTableType: TableType = {
columnDefinitions: [
{
field: 'id',
name: 'Tag name',
render: (id: string, tag: BeatTag) => (
<ConnectedLink path={`/tag/edit/${tag.id}`}>
<TagBadge tag={tag} />
</ConnectedLink>
),
sortable: true,
width: '45%',
},
{
align: 'right',
field: 'configuration_blocks',
name: 'Configurations',
render: (configurationBlocks: ConfigurationBlock[]) => (
<div>{configurationBlocks.length}</div>
),
sortable: false,
},
{
align: 'right',
field: 'last_updated',
name: 'Last update',
render: (lastUpdate: Date) => <div>{moment(lastUpdate).fromNow()}</div>,
sortable: true,
},
],
controlDefinitions: (data: any) => ({
actions: [
{
name: 'Remove Selected',
action: 'delete',
danger: true,
},
],
filters: [],
}),
};
export const BeatDetailTagsTable: TableType = {
columnDefinitions: [
{
field: 'id',
name: 'Tag name',
render: (id: string, tag: BeatTag) => (
<ConnectedLink path={`/tag/edit/${tag.id}`}>
<TagBadge tag={tag} />
</ConnectedLink>
),
sortable: true,
width: '55%',
},
{
align: 'right',
field: 'configuration_blocks',
name: 'Configurations',
render: (configurations: ConfigurationBlock[]) => <span>{configurations.length}</span>,
sortable: true,
},
{
align: 'right',
field: 'last_updated',
name: 'Last update',
render: (lastUpdate: string) => <span>{moment(lastUpdate).fromNow()}</span>,
sortable: true,
},
],
controlDefinitions: (data: any) => ({
actions: [],
filters: [],
primaryActions: [
{
name: 'Add Tag',
action: 'add',
danger: false,
},
{
name: 'Remove Selected',
action: 'remove',
danger: true,
},
],
}),
};

View file

@ -0,0 +1,79 @@
/*
* 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,
// @ts-ignore EuiConfirmModal typings not included in current EUI
EuiConfirmModal,
EuiOverlayMask,
} from '@elastic/eui';
import React from 'react';
import { AssignmentActionType } from '../table';
interface ActionControlProps {
action: AssignmentActionType;
danger?: boolean;
name: string;
showWarning?: boolean;
warningHeading?: string;
warningMessage?: string;
actionHandler(action: AssignmentActionType, payload?: any): void;
}
interface ActionControlState {
showModal: boolean;
}
export class ActionControl extends React.PureComponent<ActionControlProps, ActionControlState> {
constructor(props: ActionControlProps) {
super(props);
this.state = {
showModal: false,
};
}
public render() {
const {
action,
actionHandler,
danger,
name,
showWarning,
warningHeading,
warningMessage,
} = this.props;
return (
<div>
<EuiButton
color={danger ? 'danger' : 'primary'}
onClick={
showWarning ? () => this.setState({ showModal: true }) : () => actionHandler(action)
}
>
{name}
</EuiButton>
{this.state.showModal && (
<EuiOverlayMask>
<EuiConfirmModal
buttonColor={danger ? 'danger' : 'primary'}
cancelButtonText="Cancel"
confirmButtonText="Confirm"
onConfirm={() => {
actionHandler(action);
this.setState({ showModal: false });
}}
onCancel={() => this.setState({ showModal: false })}
title={warningHeading ? warningHeading : 'Confirm'}
>
{warningMessage}
</EuiConfirmModal>
</EuiOverlayMask>
)}
</div>
);
}
}

View file

@ -0,0 +1,7 @@
/*
* 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 { OptionControl } from './option_control';

View file

@ -0,0 +1,54 @@
/*
* 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 from 'react';
import { AssignmentComponentType, AssignmentControlSchema } from '../table';
import { AssignmentActionType } from '../table';
import { ActionControl } from './action_control';
import { PopoverControl } from './popover_control';
import { SelectionCount } from './selection_count';
import { TagBadgeList } from './tag_badge_list';
interface OptionControlProps {
items: any[];
schema: AssignmentControlSchema;
selectionCount: number;
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export const OptionControl = (props: OptionControlProps) => {
const {
actionHandler,
items,
schema,
schema: { action, danger, name, showWarning, warningHeading, warningMessage },
selectionCount,
} = props;
switch (schema.type) {
case AssignmentComponentType.Action:
if (!action) {
throw Error('Action cannot be undefined');
}
return (
<ActionControl
actionHandler={actionHandler}
action={action}
danger={danger}
name={name}
showWarning={showWarning}
warningHeading={warningHeading}
warningMessage={warningMessage}
/>
);
case AssignmentComponentType.Popover:
return <PopoverControl {...props} />;
case AssignmentComponentType.SelectionCount:
return <SelectionCount selectionCount={selectionCount} />;
case AssignmentComponentType.TagBadgeList:
return <TagBadgeList actionHandler={actionHandler} items={items} />;
}
return <div>{schema.type}</div>;
};

View file

@ -0,0 +1,90 @@
/*
* 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, EuiPopover } from '@elastic/eui';
import React from 'react';
import { AssignmentActionType } from '../table';
import { AssignmentControlSchema } from '../table/assignment_schema';
import { OptionControl } from './option_control';
interface PopoverControlProps {
items: any[];
schema: AssignmentControlSchema;
selectionCount: number;
actionHandler(action: AssignmentActionType, payload?: any): void;
}
interface PopoverControlState {
showPopover: boolean;
}
export class PopoverControl extends React.PureComponent<PopoverControlProps, PopoverControlState> {
constructor(props: PopoverControlProps) {
super(props);
this.state = {
showPopover: false,
};
}
public componentDidMount() {
const {
schema: { lazyLoad },
} = this.props;
if (!lazyLoad) {
this.props.actionHandler(AssignmentActionType.Reload);
}
}
public render() {
const {
actionHandler,
items,
schema: { children, lazyLoad, name },
selectionCount,
} = this.props;
return (
<EuiPopover
button={
<EuiButton
color="primary"
iconSide="right"
iconType="arrowDown"
onClick={() => {
if (lazyLoad) {
actionHandler(AssignmentActionType.Reload);
}
this.setState({
showPopover: true,
});
}}
>
{name}
</EuiButton>
}
closePopover={() => {
this.setState({ showPopover: false });
}}
id="assignmentList"
isOpen={this.state.showPopover}
panelPaddingSize="s"
withTitle
>
{children
? children.map(def => (
<OptionControl
actionHandler={actionHandler}
schema={def}
selectionCount={selectionCount}
key={def.name}
items={items}
/>
))
: null}
</EuiPopover>
);
}
}

View file

@ -0,0 +1,17 @@
/*
* 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 from 'react';
interface SelectionCountProps {
selectionCount: number;
}
export const SelectionCount = (props: SelectionCountProps) => (
<div>
{props.selectionCount} {`item${props.selectionCount === 1 ? '' : 's'}`} selected
</div>
);

View file

@ -0,0 +1,55 @@
/*
* 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 React from 'react';
import { TABLE_CONFIG } from '../../../common/constants';
import { TagBadge } from '../tag/tag_badge';
interface TagAssignmentProps {
tag: any;
assignTag(id: string): void;
}
interface TagAssignmentState {
isFetchingTags: boolean;
}
export class TagAssignment extends React.PureComponent<TagAssignmentProps, TagAssignmentState> {
constructor(props: TagAssignmentProps) {
super(props);
this.state = {
isFetchingTags: false,
};
}
public render() {
const {
assignTag,
tag,
tag: { id },
} = this.props;
return (
<EuiFlexGroup gutterSize="xs" key={id}>
{this.state.isFetchingTags && (
<EuiFlexItem>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
)}
<EuiFlexItem>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
onClick={() => assignTag(id)}
onClickAriaLabel={id}
tag={tag}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}

View file

@ -0,0 +1,29 @@
/*
* 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 } from '@elastic/eui';
import React from 'react';
import { AssignmentActionType } from '../table/table';
import { TagAssignment } from './tag_assignment';
interface TagBadgeListProps {
items: any[];
actionHandler(action: AssignmentActionType, payload?: any): void;
}
export const TagBadgeList = (props: TagBadgeListProps) => (
// @ts-ignore direction prop type "column" not defined in current EUI version
<EuiFlexGroup direction="column" gutterSize="xs">
{props.items.map((item: any) => (
<EuiFlexItem key={`${item.id}`}>
<TagAssignment
tag={item}
assignTag={(id: string) => props.actionHandler(AssignmentActionType.Assign, id)}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
);

View file

@ -0,0 +1,224 @@
/*
* 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.
*/
// @ts-ignore
import Formsy, { addValidationRule, FieldValue, FormData } from 'formsy-react';
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 {
FormsyEuiCodeEditor,
FormsyEuiFieldText,
FormsyEuiMultiFieldText,
FormsyEuiPasswordText,
FormsyEuiSelect,
} from '../../inputs';
addValidationRule('isHosts', (form: FormData, values: FieldValue | string[]) => {
if (values && values.length > 0 && values instanceof Array) {
return values.reduce((pass: boolean, value: string) => {
if (
pass &&
value.match(
new RegExp(
'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$'
)
) !== null
) {
return true;
}
return false;
}, true);
} else {
return true;
}
});
addValidationRule('isString', (values: FormData, value: FieldValue) => {
return true;
});
addValidationRule('isPeriod', (values: FormData, value: FieldValue) => {
// TODO add more validation
return true;
});
addValidationRule('isPath', (values: FormData, value: FieldValue) => {
// TODO add more validation
return value && value.length > 0;
});
addValidationRule('isPaths', (values: FormData, value: FieldValue) => {
// TODO add more validation
return true;
});
addValidationRule('isYaml', (values: FormData, value: FieldValue) => {
try {
const stuff = yaml.safeLoad(value || '');
if (typeof stuff === 'string') {
return false;
}
return true;
} catch (e) {
return false;
}
});
interface ComponentProps {
values: ConfigurationBlock;
schema: YamlConfigSchema[];
id: string;
canSubmit(canIt: boolean): any;
onSubmit(modal: any): any;
}
export class ConfigForm extends React.Component<ComponentProps, any> {
private form = React.createRef<HTMLButtonElement>();
constructor(props: ComponentProps) {
super(props);
this.state = {
canSubmit: false,
};
}
public enableButton = () => {
this.setState({
canSubmit: true,
});
this.props.canSubmit(true);
};
public disableButton = () => {
this.setState({
canSubmit: false,
});
this.props.canSubmit(false);
};
public submit = () => {
if (this.form.current) {
this.form.current.click();
}
};
public onValidSubmit = <ModelType extends any>(model: ModelType) => {
const processed = JSON.parse(JSON.stringify(model), (key, value) => {
return _.isObject(value) && !_.isArray(value)
? _.mapKeys(value, (v, k: string) => {
return k.replace(/{{[^{}]+}}/g, (token: string) => {
return model[token.replace(/[{}]+/g, '')] || 'error';
});
})
: value;
});
this.props.schema.forEach(s => {
if (s.ui.transform && s.ui.transform === 'removed') {
delete processed[s.id];
}
});
this.props.onSubmit(processed);
};
public render() {
return (
<div>
<br />
<Formsy
onValidSubmit={this.onValidSubmit}
onValid={this.enableButton}
onInvalid={this.disableButton}
>
{this.props.schema.map(schema => {
switch (schema.ui.type) {
case 'input':
return (
<FormsyEuiFieldText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
name={schema.id}
helpText={schema.ui.helpText}
label={schema.ui.label}
validations={schema.validations}
validationError={schema.error}
required={schema.required || false}
/>
);
case 'password':
return (
<FormsyEuiPasswordText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
name={schema.id}
helpText={schema.ui.helpText}
label={schema.ui.label}
validations={schema.validations}
validationError={schema.error}
required={schema.required || false}
/>
);
case 'multi-input':
return (
<FormsyEuiMultiFieldText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
name={schema.id}
helpText={schema.ui.helpText}
label={schema.ui.label}
validations={schema.validations}
validationError={schema.error}
required={schema.required}
/>
);
case 'select':
return (
<FormsyEuiSelect
key={schema.id}
id={schema.id}
name={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
helpText={schema.ui.helpText}
label={schema.ui.label}
options={[{ value: '', text: 'Please Select An Option' }].concat(
schema.options || []
)}
validations={schema.validations}
validationError={schema.error}
required={schema.required}
/>
);
case 'code':
return (
<FormsyEuiCodeEditor
key={`${schema.id}-${this.props.id}`}
mode="yaml"
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
name={schema.id}
helpText={schema.ui.helpText}
label={schema.ui.label}
options={schema.options ? schema.options : []}
validations={schema.validations}
validationError={schema.error}
required={schema.required}
/>
);
}
})}
<button
type="submit"
style={{ display: 'none' }}
disabled={!this.state.canSubmit}
ref={this.form}
/>
</Formsy>
</div>
);
}
}

View file

@ -0,0 +1,144 @@
/*
* 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,
EuiButtonEmpty,
// @ts-ignore
EuiCodeEditor,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormRow,
// @ts-ignore
EuiHorizontalRule,
// @ts-ignore
EuiSearchBar,
// @ts-ignore
EuiSelect,
// @ts-ignore
EuiTabbedContent,
EuiTitle,
} from '@elastic/eui';
import React from 'react';
import { ConfigurationBlock } from '../../../../common/domain_types';
import { supportedConfigs } from '../../../config_schemas';
import { ConfigForm } from './config_form';
interface ComponentProps {
configBlock?: ConfigurationBlock;
onClose(): any;
onSave(config: ConfigurationBlock): any;
}
export class ConfigView extends React.Component<ComponentProps, any> {
private form = React.createRef<any>();
private editMode: boolean;
constructor(props: any) {
super(props);
this.editMode = props.configBlock !== undefined;
this.state = {
valid: false,
configBlock: props.configBlock || {
type: supportedConfigs[0].value,
},
};
}
public onValueChange = (field: string) => (e: any) => {
const value = e.currentTarget ? e.currentTarget.value : e;
this.setState((state: any) => ({
configBlock: {
...state.configBlock,
[field]: value,
},
}));
};
public render() {
return (
<EuiFlyout onClose={this.props.onClose}>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>{this.editMode ? 'Edit Configuration' : 'Add Configuration'}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFormRow label="Configuration type">
<EuiSelect
options={supportedConfigs}
value={this.state.configBlock.type}
disabled={this.editMode}
onChange={this.onValueChange('type')}
/>
</EuiFormRow>
<EuiFormRow label="Configuration description">
<EuiFieldText
value={this.state.configBlock.description}
onChange={this.onValueChange('description')}
placeholder="Description (optional)"
/>
</EuiFormRow>
<h3>
Config for&nbsp;
{
(supportedConfigs.find(config => this.state.configBlock.type === config.value) as any)
.text
}
</h3>
<EuiHorizontalRule />
<ConfigForm
// tslint:disable-next-line:no-console
onSubmit={data => {
this.props.onSave({
...this.state.configBlock,
configs: [data],
});
this.props.onClose();
}}
canSubmit={canIt => this.setState({ valid: canIt })}
ref={this.form}
values={this.state.configBlock}
id={
(supportedConfigs.find(config => this.state.configBlock.type === config.value) as any)
.value
}
schema={
(supportedConfigs.find(config => this.state.configBlock.type === config.value) as any)
.config
}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={this.props.onClose}>
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={!this.state.valid}
fill
onClick={() => {
if (this.form.current) {
this.form.current.submit();
}
}}
>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
}

View file

@ -0,0 +1,8 @@
/*
* 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 { TagBadge } from './tag_badge';
export { TagEdit } from './tag_edit';

View file

@ -0,0 +1,39 @@
/*
* 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 { EuiBadge } from '@elastic/eui';
import React from 'react';
import { TABLE_CONFIG } from '../../../common/constants';
interface TagBadgeProps {
iconType?: any;
onClick?: () => void;
onClickAriaLabel?: string;
maxIdRenderSize?: number;
tag: { color?: string; id: string };
}
export const TagBadge = (props: TagBadgeProps) => {
const {
iconType,
onClick,
onClickAriaLabel,
tag: { color, id },
} = props;
const maxIdRenderSize = props.maxIdRenderSize || TABLE_CONFIG.TRUNCATE_TAG_LENGTH;
const idToRender = id.length > maxIdRenderSize ? `${id.substring(0, maxIdRenderSize)}...` : id;
return (
<EuiBadge
color={color || 'primary'}
iconType={iconType}
onClick={onClick}
onClickAriaLabel={onClickAriaLabel}
>
{idToRender}
</EuiBadge>
);
};

View file

@ -0,0 +1,223 @@
/*
* 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,
// @ts-ignore
EuiColorPicker,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
// @ts-ignore
EuiForm,
EuiFormRow,
EuiHorizontalRule,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
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 { ConfigView } from './config_view';
import { TagBadge } from './tag_badge';
interface TagEditProps {
mode: 'edit' | 'create';
tag: Pick<BeatTag, Exclude<keyof BeatTag, 'last_updated'>>;
onDetachBeat: (beatIds: string[]) => void;
onTagChange: (field: keyof BeatTag, value: string) => any;
attachedBeats: CMBeat[] | null;
}
interface TagEditState {
showFlyout: boolean;
tableRef: any;
selectedConfigIndex?: number;
}
export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
constructor(props: TagEditProps) {
super(props);
this.state = {
showFlyout: false,
tableRef: React.createRef(),
};
}
public render() {
const { tag, attachedBeats } = this.props;
return (
<div>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>Tag details</h3>
</EuiTitle>
<EuiText color="subdued">
<p>
Tags will apply the configurations below to all beats assigned this tag.
<br />
The tag type defines the options available.
</p>
</EuiText>
<div>
<TagBadge tag={{ color: tag.color || '#FF0', id: tag.id }} />
</div>
</EuiFlexItem>
<EuiFlexItem>
<EuiForm>
<EuiFormRow
label="Tag Name"
isInvalid={!!this.getNameError(tag.id)}
error={this.getNameError(tag.id) || undefined}
>
<EuiFieldText
name="name"
isInvalid={!!this.getNameError(tag.id)}
onChange={this.updateTag('id')}
disabled={this.props.mode === 'edit'}
value={tag.id}
placeholder="Tag name (required)"
/>
</EuiFormRow>
{this.props.mode === 'create' && (
<EuiFormRow label="Tag Color">
<EuiColorPicker color={tag.color} onChange={this.updateTag('color')} />
</EuiFormRow>
)}
</EuiForm>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiHorizontalRule />
<EuiFlexGroup
alignItems={
tag.configuration_blocks && tag.configuration_blocks.length ? 'stretch' : 'center'
}
>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>Tag Configurations</h3>
</EuiTitle>
<EuiText color="subdued">
<p>
Tags can contain multiple configurations. These configurations can repeat or mix
types as necessary. For example, you may utilize three metricbeat configurations
alongside one input and filebeat configuration.
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<div>
<ConfigList
configs={tag.configuration_blocks}
onConfigClick={(action: string, config: ConfigurationBlock) => {
const selectedIndex = tag.configuration_blocks.findIndex(c => {
return isEqual(config, c);
});
if (action === 'delete') {
const configs = [...tag.configuration_blocks];
configs.splice(selectedIndex, 1);
this.updateTag('configuration_blocks', configs);
} else {
this.setState({
showFlyout: true,
selectedConfigIndex: selectedIndex,
});
}
}}
/>
<br />
<EuiButton
onClick={() => {
this.setState({ showFlyout: true });
}}
>
Add configuration
</EuiButton>
</div>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{attachedBeats && (
<div>
<EuiHorizontalRule />
<EuiTitle size="xs">
<h3>Attached Beats</h3>
</EuiTitle>
<Table
assignmentOptions={{
schema: tagConfigAssignmentOptions,
items: [],
type: 'primary',
actionHandler: this.handleAssignmentActions,
}}
items={attachedBeats}
ref={this.state.tableRef}
type={BeatsTableType}
/>
</div>
)}
{this.state.showFlyout && (
<ConfigView
configBlock={
this.state.selectedConfigIndex !== undefined
? tag.configuration_blocks[this.state.selectedConfigIndex]
: undefined
}
onClose={() => this.setState({ showFlyout: false, selectedConfigIndex: undefined })}
onSave={(config: any) => {
this.setState({ showFlyout: false, selectedConfigIndex: undefined });
if (this.state.selectedConfigIndex !== undefined) {
const configs = [...tag.configuration_blocks];
configs[this.state.selectedConfigIndex] = config;
this.updateTag('configuration_blocks', configs);
} else {
this.updateTag('configuration_blocks', [
...(tag.configuration_blocks || []),
config,
]);
}
}}
/>
)}
</div>
);
}
private getNameError = (name: string) => {
if (name && name !== '' && name.search(/^[a-zA-Z0-9-]+$/) === -1) {
return 'Tag name must consist of letters, numbers, and dashes only';
} else {
return false;
}
};
private handleAssignmentActions = (action: AssignmentActionType) => {
switch (action) {
case AssignmentActionType.Delete:
const { selection } = this.state.tableRef.current.state;
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);
}

View file

@ -0,0 +1,370 @@
/*
* 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 { YamlConfigSchema } from './lib/lib';
const filebeatInputConfig: YamlConfigSchema[] = [
{
id: 'paths',
ui: {
label: 'Paths',
type: 'multi-input',
},
validations: 'isPaths',
error: 'One file path per line',
required: true,
},
{
id: 'other',
ui: {
label: 'Other Config',
type: 'code',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
},
];
const filebeatModuleConfig: YamlConfigSchema[] = [
{
id: 'module',
ui: {
label: 'Module',
type: 'select',
},
options: [
{
value: 'apache2',
text: 'apache2',
},
{
value: 'auditd',
text: 'auditd',
},
{
value: 'elasticsearch',
text: 'elasticsearch',
},
{
value: 'haproxy',
text: 'haproxy',
},
{
value: 'icinga',
text: 'icinga',
},
{
value: 'iis',
text: 'iis',
},
{
value: 'kafka',
text: 'kafka',
},
{
value: 'kibana',
text: 'kibana',
},
{
value: 'logstash',
text: 'logstash',
},
{
value: 'mongodb',
text: 'mongodb',
},
{
value: 'mysql',
text: 'mysql',
},
{
value: 'nginx',
text: 'nginx',
},
{
value: 'osquery',
text: 'osquery',
},
{
value: 'postgresql',
text: 'postgresql',
},
{
value: 'redis',
text: 'redis',
},
{
value: 'system',
text: 'system',
},
{
value: 'traefik',
text: 'traefik',
},
],
error: 'Please select a module',
required: true,
},
{
id: 'other',
ui: {
label: 'Other Config',
type: 'code',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
},
];
const metricbeatModuleConfig: YamlConfigSchema[] = [
{
id: 'module',
ui: {
label: 'Module',
type: 'select',
},
options: [
{
value: 'aerospike',
text: 'aerospike',
},
{
value: 'apache',
text: 'apache',
},
{
value: 'ceph',
text: 'ceph',
},
{
value: 'couchbase',
text: 'couchbase',
},
{
value: 'docker',
text: 'docker',
},
{
value: 'dropwizard',
text: 'dropwizard',
},
{
value: 'elasticsearch',
text: 'elasticsearch',
},
{
value: 'envoyproxy',
text: 'envoyproxy',
},
{
value: 'etcd',
text: 'etcd',
},
{
value: 'golang',
text: 'golang',
},
{
value: 'graphite',
text: 'graphite',
},
{
value: 'haproxy',
text: 'haproxy',
},
{
value: 'http',
text: 'http',
},
{
value: 'jolokia',
text: 'jolokia',
},
{
value: 'kafka',
text: 'kafka',
},
{
value: 'kibana',
text: 'kibana',
},
{
value: 'kubernetes',
text: 'kubernetes',
},
{
value: 'kvm',
text: 'kvm',
},
{
value: 'logstash',
text: 'logstash',
},
{
value: 'memcached',
text: 'memcached',
},
{
value: 'mongodb',
text: 'mongodb',
},
{
value: 'munin',
text: 'munin',
},
{
value: 'mysql',
text: 'mysql',
},
{
value: 'nginx',
text: 'nginx',
},
{
value: 'php_fpm',
text: 'php_fpm',
},
{
value: 'postgresql',
text: 'postgresql',
},
{
value: 'prometheus',
text: 'prometheus',
},
{
value: 'rabbitmq',
text: 'rabbitmq',
},
{
value: 'redis',
text: 'redis',
},
{
value: 'system',
text: 'system',
},
{
value: 'traefik',
text: 'traefik',
},
{
value: 'uwsgi',
text: 'uwsgi',
},
{
value: 'vsphere',
text: 'vsphere',
},
{
value: 'windows',
text: 'windows',
},
{
value: 'zookeeper',
text: 'zookeeper',
},
],
error: 'Please select a module',
required: true,
},
{
id: 'hosts',
ui: {
label: 'Hosts',
type: 'multi-input',
},
validations: 'isHosts',
error: 'One file host per line',
required: false,
},
{
id: 'period',
ui: {
label: 'Period',
type: 'input',
},
defaultValue: '10s',
validations: 'isPeriod',
error: 'Invalid Period, must be formatted as `10s` for 10 seconds',
required: true,
},
{
id: 'other',
ui: {
label: 'Other Config',
type: 'code',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
},
];
const outputConfig: YamlConfigSchema[] = [
{
id: 'output',
ui: {
label: 'Output Type',
type: 'select',
transform: 'removed',
},
options: [
{
value: 'elasticsearch',
text: 'Elasticsearch',
},
{
value: 'logstash',
text: 'Logstash',
},
{
value: 'kafka',
text: 'Kafka',
},
{
value: 'console',
text: 'Console',
},
],
error: 'Please select an output type',
required: true,
},
{
id: '{{output}}.hosts',
ui: {
label: 'Hosts',
type: 'multi-input',
},
validations: 'isHosts',
error: 'One file host per line',
parseValidResult: v => v.split('\n'),
},
{
id: '{{output}}.username',
ui: {
label: 'Username',
type: 'input',
},
validations: 'isString',
error: 'Unprocessable username',
},
{
id: '{{output}}.password',
ui: {
label: 'Password',
type: 'password',
},
validations: 'isString',
error: 'Unprocessable password',
},
];
export const supportedConfigs = [
{ text: 'Filebeat Input', value: 'filebeat.inputs', config: filebeatInputConfig },
{ text: 'Filebeat Module', value: 'filebeat.modules', config: filebeatModuleConfig },
{ text: 'Metricbeat Module', value: 'metricbeat.modules', config: metricbeatModuleConfig },
{ text: 'Output', value: 'output', config: outputConfig },
];

View file

@ -0,0 +1,89 @@
/*
* 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 from 'react';
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { FrontendLibs } from '../lib/lib';
import { RendererFunction } from '../utils/typed_react';
interface WithKueryAutocompletionLifecycleProps {
libs: FrontendLibs;
fieldPrefix?: string;
children: RendererFunction<{
isLoadingSuggestions: boolean;
loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void;
suggestions: AutocompleteSuggestion[];
}>;
}
interface WithKueryAutocompletionLifecycleState {
// lacking cancellation support in the autocompletion api,
// this is used to keep older, slower requests from clobbering newer ones
currentRequest: {
expression: string;
cursorPosition: number;
} | null;
suggestions: AutocompleteSuggestion[];
}
export class WithKueryAutocompletion extends React.Component<
WithKueryAutocompletionLifecycleProps,
WithKueryAutocompletionLifecycleState
> {
public readonly state: WithKueryAutocompletionLifecycleState = {
currentRequest: null,
suggestions: [],
};
public render() {
const { currentRequest, suggestions } = this.state;
return this.props.children({
isLoadingSuggestions: currentRequest !== null,
loadSuggestions: this.loadSuggestions,
suggestions,
});
}
private loadSuggestions = async (
expression: string,
cursorPosition: number,
maxSuggestions?: number
) => {
this.setState({
currentRequest: {
expression,
cursorPosition,
},
suggestions: [],
});
let suggestions: any[] = [];
try {
suggestions = await this.props.libs.elasticsearch.getSuggestions(
expression,
cursorPosition,
this.props.fieldPrefix
);
} catch (e) {
suggestions = [];
}
this.setState(
state =>
state.currentRequest &&
state.currentRequest.expression !== expression &&
state.currentRequest.cursorPosition !== cursorPosition
? state // ignore this result, since a newer request is in flight
: {
...state,
currentRequest: null,
suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions,
}
);
};
}

View file

@ -0,0 +1,99 @@
/*
* 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 { parse, stringify } from 'querystring';
import React from 'react';
import { withRouter } from 'react-router-dom';
import { FlatObject } from '../app';
import { RendererFunction } from '../utils/typed_react';
type StateCallback<T> = (previousState: T) => T;
export interface URLStateProps<URLState = object> {
goTo: (path: string) => void;
setUrlState: (
newState:
| Partial<FlatObject<URLState>>
| StateCallback<URLState>
| Promise<StateCallback<URLState>>
) => void;
urlState: URLState;
}
interface ComponentProps<URLState extends object> {
history: any;
match: any;
children: RendererFunction<URLStateProps<URLState>>;
}
export class WithURLStateComponent<URLState extends object> extends React.Component<
ComponentProps<URLState>
> {
private get URLState(): URLState {
// slice because parse does not account for the initial ? in the search string
return parse(decodeURIComponent(this.props.history.location.search).substring(1)) as URLState;
}
private historyListener: (() => void) | null = null;
public componentWillUnmount() {
if (this.historyListener) {
this.historyListener();
}
}
public render() {
return this.props.children({
goTo: this.goTo,
setUrlState: this.setURLState,
urlState: this.URLState || {},
});
}
private setURLState = async (
state:
| Partial<FlatObject<URLState>>
| StateCallback<URLState>
| Promise<StateCallback<URLState>>
) => {
let newState;
const pastState = this.URLState;
if (typeof state === 'function') {
newState = await state(pastState);
} else {
newState = state;
}
const search: string = stringify({
...(pastState as any),
...(newState as any),
});
const newLocation = {
...this.props.history.location,
search,
};
this.props.history.replace(newLocation);
this.forceUpdate();
};
private goTo = (path: string) => {
this.props.history.push({
pathname: path,
search: this.props.history.location.search,
});
};
}
export const WithURLState = withRouter<any>(WithURLStateComponent);
export function withUrlState<OP>(UnwrappedComponent: React.ComponentType<OP>): React.SFC<any> {
return (origProps: OP) => {
return (
<WithURLState>
{(URLProps: URLStateProps) => <UnwrappedComponent {...URLProps} {...origProps} />}
</WithURLState>
);
};
}

View file

@ -0,0 +1,30 @@
/*
* 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 from 'react';
import { BASE_PATH } from '../common/constants';
import { compose } from './lib/compose/kibana';
import { FrontendLibs } from './lib/lib';
// import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
// import { ThemeProvider } from 'styled-components';
import { PageRouter } from './router';
// TODO use theme provided from parentApp when kibana supports it
import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json';
import '@elastic/eui/dist/eui_theme_light.css';
import { ThemeProvider } from 'styled-components';
function startApp(libs: FrontendLibs) {
libs.framework.registerManagementSection('beats', 'Beats Management', BASE_PATH);
libs.framework.render(
<ThemeProvider theme={{ eui: euiVars }}>
<PageRouter libs={libs} />
</ThemeProvider>
);
}
startApp(compose());

View file

@ -0,0 +1,136 @@
/*
* 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 } from '../../../common/domain_types';
import { supportedConfigs } from '../../config_schemas';
import { CMTagsAdapter } from '../adapters/tags/adapter_types';
import { TagsLib } from '../tags';
describe('Tags Client Domain Lib', () => {
let tagsLib: TagsLib;
beforeEach(async () => {
tagsLib = new TagsLib({} as CMTagsAdapter, supportedConfigs);
});
it('should use helper function to convert users yaml in tag to config object', async () => {
const convertedTag = tagsLib.userConfigsToJson([
{
id: 'foo',
configuration_blocks: [
{
type: 'filebeat.inputs',
description: 'string',
configs: [{ paths: ['adad/adasd'], other: "something: 'here'" }],
},
],
color: 'red',
last_updated: new Date(),
} as BeatTag,
]);
expect(convertedTag.length).toBe(1);
expect(convertedTag[0].configuration_blocks.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other');
expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('something');
expect((convertedTag[0].configuration_blocks[0].configs[0] as any).something).toBe('here');
});
it('should use helper function to convert user config to json with undefined `other`', async () => {
const convertedTag = tagsLib.userConfigsToJson([
{
id: 'fsdfsdfs',
color: '#DD0A73',
configuration_blocks: [
{
type: 'filebeat.inputs',
description: 'sdfsdf',
configs: [{ paths: ['sdfsfsdf'], other: undefined }],
},
],
last_updated: '2018-09-04T15:52:08.983Z',
} as any,
]);
expect(convertedTag.length).toBe(1);
expect(convertedTag[0].configuration_blocks.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other');
});
it('should use helper function to convert users yaml in tag to config object, where empty other leads to no other fields saved', async () => {
const convertedTag = tagsLib.userConfigsToJson([
{
id: 'foo',
configuration_blocks: [
{
type: 'filebeat.inputs',
description: 'string',
configs: [{ paths: ['adad/adasd'], other: '' }],
},
],
color: 'red',
last_updated: new Date(),
} as BeatTag,
]);
expect(convertedTag.length).toBe(1);
expect(convertedTag[0].configuration_blocks.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('other');
});
it('should use helper function to convert config object to users yaml', async () => {
const convertedTag = tagsLib.jsonConfigToUserYaml([
{
id: 'fsdfsdfs',
color: '#DD0A73',
configuration_blocks: [
{
type: 'filebeat.inputs',
description: 'sdfsdf',
configs: [{ paths: ['sdfsfsdf'], something: 'here' }],
},
],
last_updated: '2018-09-04T15:52:08.983Z',
} as any,
]);
expect(convertedTag.length).toBe(1);
expect(convertedTag[0].configuration_blocks.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('something');
expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('other');
expect(convertedTag[0].configuration_blocks[0].configs[0].other).toBe('something: here\n');
});
it('should use helper function to convert config object to users yaml with empty `other`', async () => {
const convertedTag = tagsLib.jsonConfigToUserYaml([
{
id: 'fsdfsdfs',
color: '#DD0A73',
configuration_blocks: [
{
type: 'filebeat.inputs',
description: undefined,
configs: [{ paths: ['sdfsfsdf'] }],
},
],
last_updated: '2018-09-04T15:52:08.983Z',
} as any,
]);
expect(convertedTag.length).toBe(1);
expect(convertedTag[0].configuration_blocks.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs.length).toBe(1);
expect(convertedTag[0].configuration_blocks[0].configs[0]).not.toHaveProperty('something');
expect(convertedTag[0].configuration_blocks[0].configs[0]).toHaveProperty('other');
expect(convertedTag[0].configuration_blocks[0].configs[0].other).toBe('');
});
});

View file

@ -0,0 +1,36 @@
/*
* 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 { CMBeat } from '../../../../common/domain_types';
export interface CMBeatsAdapter {
get(id: string): Promise<CMBeat | null>;
update(id: string, beatData: Partial<CMBeat>): Promise<boolean>;
getBeatsWithTag(tagId: string): Promise<CMBeat[]>;
getAll(ESQuery?: any): Promise<CMBeat[]>;
removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise<BeatsRemovalReturn[]>;
assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise<CMAssignmentReturn[]>;
getBeatWithToken(enrollmentToken: string): Promise<CMBeat | null>;
}
export interface BeatsTagAssignment {
beatId: string;
tag: string;
idxInRequest?: number;
}
interface BeatsReturnedTagAssignment {
status: number | null;
result?: string;
}
export interface CMAssignmentReturn {
assignments: BeatsReturnedTagAssignment[];
}
export interface BeatsRemovalReturn {
removals: BeatsReturnedTagAssignment[];
}

View file

@ -0,0 +1,105 @@
/*
* 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 { omit } from 'lodash';
import { CMBeat } from '../../../../common/domain_types';
import {
BeatsRemovalReturn,
BeatsTagAssignment,
CMAssignmentReturn,
CMBeatsAdapter,
} from './adapter_types';
export class MemoryBeatsAdapter implements CMBeatsAdapter {
private beatsDB: CMBeat[];
constructor(beatsDB: CMBeat[]) {
this.beatsDB = beatsDB;
}
public async get(id: string) {
return this.beatsDB.find(beat => beat.id === id) || null;
}
public async update(id: string, beatData: Partial<CMBeat>): Promise<boolean> {
const index = this.beatsDB.findIndex(beat => beat.id === id);
if (index === -1) {
return false;
}
this.beatsDB[index] = { ...this.beatsDB[index], ...beatData };
return true;
}
public async getAll() {
return this.beatsDB.map<CMBeat>((beat: any) => omit(beat, ['access_token']));
}
public async getBeatsWithTag(tagId: string): Promise<CMBeat[]> {
return this.beatsDB.map<CMBeat>((beat: any) => omit(beat, ['access_token']));
}
public async getBeatWithToken(enrollmentToken: string): Promise<CMBeat | null> {
return this.beatsDB.map<CMBeat>((beat: any) => omit(beat, ['access_token']))[0];
}
public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise<BeatsRemovalReturn[]> {
const beatIds = removals.map(r => r.beatId);
const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => {
const tagData = removals.find(r => r.beatId === beat.id);
if (tagData) {
if (beat.tags) {
beat.tags = beat.tags.filter(tag => tag !== tagData.tag);
}
}
const removalsForBeat = removals.filter(r => r.beatId === beat.id);
if (removalsForBeat.length) {
removalsForBeat.forEach((assignment: BeatsTagAssignment) => {
if (beat.tags) {
beat.tags = beat.tags.filter(tag => tag !== assignment.tag);
}
});
}
return beat;
});
return response.map<any>((item: CMBeat, resultIdx: number) => ({
idxInRequest: removals[resultIdx].idxInRequest,
result: 'updated',
status: 200,
}));
}
public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise<CMAssignmentReturn[]> {
const beatIds = assignments.map(r => r.beatId);
this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => {
// get tags that need to be assigned to this beat
const tags = assignments
.filter(a => a.beatId === beat.id)
.map((t: BeatsTagAssignment) => t.tag);
if (tags.length > 0) {
if (!beat.tags) {
beat.tags = [];
}
const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t));
if (nonExistingTags.length > 0) {
beat.tags = beat.tags.concat(nonExistingTags);
}
}
return beat;
});
return assignments.map<any>((item: BeatsTagAssignment, resultIdx: number) => ({
idxInRequest: assignments[resultIdx].idxInRequest,
result: 'updated',
status: 200,
}));
}
}

View file

@ -0,0 +1,57 @@
/*
* 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 { CMBeat } from '../../../../common/domain_types';
import { RestAPIAdapter } from '../rest_api/adapter_types';
import {
BeatsRemovalReturn,
BeatsTagAssignment,
CMAssignmentReturn,
CMBeatsAdapter,
} from './adapter_types';
export class RestBeatsAdapter implements CMBeatsAdapter {
constructor(private readonly REST: RestAPIAdapter) {}
public async get(id: string): Promise<CMBeat | null> {
return await this.REST.get<CMBeat>(`/api/beats/agent/${id}`);
}
public async getBeatWithToken(enrollmentToken: string): Promise<CMBeat | null> {
const beat = await this.REST.get<CMBeat>(`/api/beats/agent/unknown/${enrollmentToken}`);
return beat;
}
public async getAll(ESQuery?: any): Promise<CMBeat[]> {
return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents/all', { ESQuery })).beats;
}
public async getBeatsWithTag(tagId: string): Promise<CMBeat[]> {
return (await this.REST.get<{ beats: CMBeat[] }>(`/api/beats/agents/tag/${tagId}`)).beats;
}
public async update(id: string, beatData: Partial<CMBeat>): Promise<boolean> {
await this.REST.put<{ success: true }>(`/api/beats/agent/${id}`, beatData);
return true;
}
public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise<BeatsRemovalReturn[]> {
return (await this.REST.post<{ removals: BeatsRemovalReturn[] }>(
`/api/beats/agents_tags/removals`,
{
removals,
}
)).removals;
}
public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise<CMAssignmentReturn[]> {
return (await this.REST.post<{ assignments: CMAssignmentReturn[] }>(
`/api/beats/agents_tags/assignments`,
{
assignments,
}
)).assignments;
}
}

View file

@ -0,0 +1,12 @@
/*
* 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers';
export interface ElasticsearchAdapter {
convertKueryToEsQuery: (kuery: string) => Promise<string>;
getSuggestions: (kuery: string, selectionStart: any) => Promise<AutocompleteSuggestion[]>;
isKueryValid(kuery: string): boolean;
}

View file

@ -0,0 +1,29 @@
/*
* 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { ElasticsearchAdapter } from './adapter_types';
export class MemoryElasticsearchAdapter implements ElasticsearchAdapter {
constructor(
private readonly mockIsKueryValid: (kuery: string) => boolean,
private readonly mockKueryToEsQuery: (kuery: string) => string,
private readonly suggestions: AutocompleteSuggestion[]
) {}
public isKueryValid(kuery: string): boolean {
return this.mockIsKueryValid(kuery);
}
public async convertKueryToEsQuery(kuery: string): Promise<string> {
return this.mockKueryToEsQuery(kuery);
}
public async getSuggestions(
kuery: string,
selectionStart: any
): Promise<AutocompleteSuggestion[]> {
return this.suggestions;
}
}

View file

@ -0,0 +1,77 @@
/*
* 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 { 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';
export class RestElasticsearchAdapter implements ElasticsearchAdapter {
private cachedIndexPattern: any = null;
constructor(private readonly api: RestAPIAdapter, private readonly indexPatternName: string) {}
public isKueryValid(kuery: string): boolean {
try {
fromKueryExpression(kuery);
} catch (err) {
return false;
}
return true;
}
public async convertKueryToEsQuery(kuery: string): Promise<string> {
if (!this.isKueryValid(kuery)) {
return '';
}
const ast = fromKueryExpression(kuery);
const indexPattern = await this.getIndexPattern();
return JSON.stringify(toElasticsearchQuery(ast, indexPattern));
}
public async getSuggestions(
kuery: string,
selectionStart: any
): Promise<AutocompleteSuggestion[]> {
const autocompleteProvider = getAutocompleteProvider('kuery');
if (!autocompleteProvider) {
return [];
}
const config = {
get: () => true,
};
const indexPattern = await this.getIndexPattern();
const getAutocompleteSuggestions = autocompleteProvider({
config,
indexPatterns: [indexPattern],
boolFilter: null,
});
const results = getAutocompleteSuggestions({
query: kuery || '',
selectionStart,
selectionEnd: selectionStart,
});
return results;
}
private async getIndexPattern() {
if (this.cachedIndexPattern) {
return this.cachedIndexPattern;
}
const res = await this.api.get<any>(
`/api/index_patterns/_fields_for_wildcard?pattern=${this.indexPatternName}`
);
if (isEmpty(res.fields)) {
return;
}
this.cachedIndexPattern = {
fields: res.fields,
title: `${this.indexPatternName}`,
};
return this.cachedIndexPattern;
}
}

View file

@ -0,0 +1,205 @@
/*
* 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 { IModule, IScope } from 'angular';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
BufferedKibanaServiceCall,
FrameworkAdapter,
KibanaAdapterServiceRefs,
KibanaUIConfig,
} from '../../lib';
export class KibanaFrameworkAdapter implements FrameworkAdapter {
public appState: object;
private management: any;
private adapterService: KibanaAdapterServiceProvider;
private rootComponent: React.ReactElement<any> | null = null;
private uiModule: IModule;
private routes: any;
private XPackInfoProvider: any;
private xpackInfo: null | any;
private notifier: any;
private kbnUrlService: any;
private chrome: any;
constructor(
uiModule: IModule,
management: any,
routes: any,
chrome: any,
XPackInfoProvider: any,
Notifier: any
) {
this.adapterService = new KibanaAdapterServiceProvider();
this.management = management;
this.uiModule = uiModule;
this.routes = routes;
this.chrome = chrome;
this.XPackInfoProvider = XPackInfoProvider;
this.appState = {};
this.notifier = new Notifier({ location: 'Beats' });
}
public get baseURLPath(): string {
return this.chrome.getBasePath();
}
public setUISettings = (key: string, value: any) => {
this.adapterService.callOrBuffer(({ config }) => {
config.set(key, value);
});
};
public render = (component: React.ReactElement<any>) => {
this.rootComponent = component;
};
public hadValidLicense() {
if (!this.xpackInfo) {
return false;
}
return this.xpackInfo.get('features.beats_management.licenseValid', false);
}
public securityEnabled() {
if (!this.xpackInfo) {
return false;
}
return this.xpackInfo.get('features.beats_management.securityEnabled', false);
}
public registerManagementSection(pluginId: string, displayName: string, basePath: string) {
this.register(this.uiModule);
this.hookAngular(() => {
if (this.hadValidLicense() && this.securityEnabled()) {
const registerSection = () =>
this.management.register(pluginId, {
display: 'Beats', // TODO these need to be config options not hard coded in the adapter
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}`,
});
}
if (!this.securityEnabled()) {
this.notifier.error(this.xpackInfo.get(`features.beats_management.message`));
this.kbnUrlService.redirect('/management');
}
});
}
private manageAngularLifecycle($scope: any, $route: any, elem: any) {
const lastRoute = $route.current;
const deregister = $scope.$on('$locationChangeSuccess', () => {
const currentRoute = $route.current;
// if templates are the same we are on the same route
if (lastRoute.$$route.template === currentRoute.$$route.template) {
// this prevents angular from destroying scope
$route.current = lastRoute;
}
});
$scope.$on('$destroy', () => {
if (deregister) {
deregister();
}
// manually unmount component when scope is destroyed
if (elem) {
ReactDOM.unmountComponentAtNode(elem);
}
});
}
private hookAngular(done: () => any) {
this.chrome.dangerouslyGetActiveInjector().then(($injector: any) => {
const Private = $injector.get('Private');
const xpackInfo = Private(this.XPackInfoProvider);
const kbnUrlService = $injector.get('kbnUrl');
this.xpackInfo = xpackInfo;
this.kbnUrlService = kbnUrlService;
done();
});
}
private register = (adapterModule: IModule) => {
const adapter = this;
this.routes.when(`/management/beats_management/:view?/:id?/:other?/:other2?`, {
template:
'<beats-cm><div id="beatsReactRoot" style="flex-grow: 1; height: 100vh; background: #f5f5f5"></div></beats-cm>',
controllerAs: 'beatsManagement',
// tslint:disable-next-line: max-classes-per-file
controller: class BeatsManagementController {
constructor($scope: any, $route: any) {
$scope.$$postDigest(() => {
const elem = document.getElementById('beatsReactRoot');
ReactDOM.render(adapter.rootComponent as React.ReactElement<any>, elem);
adapter.manageAngularLifecycle($scope, $route, elem);
});
$scope.$onInit = () => {
$scope.topNavMenu = [];
};
}
},
});
};
}
// tslint:disable-next-line: max-classes-per-file
class KibanaAdapterServiceProvider {
public serviceRefs: KibanaAdapterServiceRefs | null = null;
public bufferedCalls: Array<BufferedKibanaServiceCall<KibanaAdapterServiceRefs>> = [];
public $get($rootScope: IScope, config: KibanaUIConfig) {
this.serviceRefs = {
config,
rootScope: $rootScope,
};
this.applyBufferedCalls(this.bufferedCalls);
return this;
}
public callOrBuffer(serviceCall: (serviceRefs: KibanaAdapterServiceRefs) => void) {
if (this.serviceRefs !== null) {
this.applyBufferedCalls([serviceCall]);
} else {
this.bufferedCalls.push(serviceCall);
}
}
public applyBufferedCalls(
bufferedCalls: Array<BufferedKibanaServiceCall<KibanaAdapterServiceRefs>>
) {
if (!this.serviceRefs) {
return;
}
this.serviceRefs.rootScope.$apply(() => {
bufferedCalls.forEach(serviceCall => {
if (!this.serviceRefs) {
return;
}
return serviceCall(this.serviceRefs);
});
});
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 { FlatObject } from '../../../app';
export interface RestAPIAdapter {
get<ResponseData>(url: string, query?: FlatObject<object>): Promise<ResponseData>;
post<ResponseData>(url: string, body?: { [key: string]: any }): Promise<ResponseData>;
delete<T>(url: string): Promise<T>;
put<ResponseData>(url: string, body?: any): Promise<ResponseData>;
}

View file

@ -0,0 +1,78 @@
/*
* 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 axios, { AxiosInstance } from 'axios';
import { FlatObject } from '../../../app';
import { RestAPIAdapter } from './adapter_types';
let globalAPI: AxiosInstance;
export class AxiosRestAPIAdapter implements RestAPIAdapter {
constructor(private readonly xsrfToken: string, private readonly basePath: string) {}
public async get<ResponseData>(url: string, query?: FlatObject<object>): Promise<ResponseData> {
return await this.REST.get(url, query ? { params: query } : {}).then(resp => resp.data);
}
public async post<ResponseData>(
url: string,
body?: { [key: string]: any }
): Promise<ResponseData> {
return await this.REST.post(url, body).then(resp => resp.data);
}
public async delete<T>(url: string): Promise<T> {
return await this.REST.delete(url).then(resp => resp.data);
}
public async put<ResponseData>(url: string, body?: any): Promise<ResponseData> {
return await this.REST.put(url, body).then(resp => resp.data);
}
private get REST() {
if (globalAPI) {
return globalAPI;
}
globalAPI = axios.create({
baseURL: this.basePath,
withCredentials: true,
responseType: 'json',
timeout: 30000,
headers: {
Accept: 'application/json',
credentials: 'same-origin',
'Content-Type': 'application/json',
'kbn-version': this.xsrfToken,
'kbn-xsrf': this.xsrfToken,
},
});
// Add a request interceptor
globalAPI.interceptors.request.use(
config => {
// Do something before request is sent
return config;
},
error => {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
globalAPI.interceptors.response.use(
response => {
// Do something with response data
return response;
},
error => {
// Do something with response error
return Promise.reject(error);
}
);
return globalAPI;
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 } from '../../../../common/domain_types';
export interface CMTagsAdapter {
getTagsWithIds(tagIds: string[]): Promise<BeatTag[]>;
delete(tagIds: string[]): Promise<boolean>;
getAll(): Promise<BeatTag[]>;
upsertTag(tag: BeatTag): Promise<BeatTag | null>;
}

View file

@ -0,0 +1,39 @@
/*
* 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 } from '../../../../common/domain_types';
import { CMTagsAdapter } from './adapter_types';
export class MemoryTagsAdapter implements CMTagsAdapter {
private tagsDB: BeatTag[] = [];
constructor(tagsDB: BeatTag[]) {
this.tagsDB = tagsDB;
}
public async getTagsWithIds(tagIds: string[]) {
return this.tagsDB.filter(tag => tagIds.includes(tag.id));
}
public async delete(tagIds: string[]) {
this.tagsDB = this.tagsDB.filter(tag => !tagIds.includes(tag.id));
return true;
}
public async getAll() {
return this.tagsDB;
}
public async upsertTag(tag: BeatTag) {
const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id);
if (existingTagIndex !== -1) {
this.tagsDB[existingTagIndex] = tag;
} else {
this.tagsDB.push(tag);
}
return tag;
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 } from '../../../../common/domain_types';
import { RestAPIAdapter } from '../rest_api/adapter_types';
import { CMTagsAdapter } from './adapter_types';
export class RestTagsAdapter implements CMTagsAdapter {
constructor(private readonly REST: RestAPIAdapter) {}
public async getTagsWithIds(tagIds: string[]): Promise<BeatTag[]> {
const tags = await this.REST.get<BeatTag[]>(`/api/beats/tags/${tagIds.join(',')}`);
return tags;
}
public async getAll(): Promise<BeatTag[]> {
return await this.REST.get<BeatTag[]>(`/api/beats/tags`);
}
public async delete(tagIds: string[]): Promise<boolean> {
return (await this.REST.delete<{ success: boolean }>(`/api/beats/tags/${tagIds.join(',')}`))
.success;
}
public async upsertTag(tag: BeatTag): Promise<BeatTag | null> {
const response = await this.REST.put<{ success: boolean }>(`/api/beats/tag/${tag.id}`, {
color: tag.color,
configuration_blocks: tag.configuration_blocks,
});
return response.success ? tag : null;
}
}

View file

@ -0,0 +1,9 @@
/*
* 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 interface CMTokensAdapter {
createEnrollmentToken(): Promise<string>;
}

View file

@ -0,0 +1,13 @@
/*
* 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 { CMTokensAdapter } from './adapter_types';
export class MemoryTokensAdapter implements CMTokensAdapter {
public async createEnrollmentToken(): Promise<string> {
return '2jnwkrhkwuehriauhweair';
}
}

View file

@ -0,0 +1,18 @@
/*
* 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 { RestAPIAdapter } from '../rest_api/adapter_types';
import { CMTokensAdapter } from './adapter_types';
export class RestTokensAdapter implements CMTokensAdapter {
constructor(private readonly REST: RestAPIAdapter) {}
public async createEnrollmentToken(): Promise<string> {
const tokens = (await this.REST.post<{ tokens: string[] }>('/api/beats/enrollment_tokens'))
.tokens;
return tokens[0];
}
}

View file

@ -0,0 +1,70 @@
/*
* 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 { flatten } from 'lodash';
import { CMBeat, CMPopulatedBeat } from './../../common/domain_types';
import {
BeatsRemovalReturn,
BeatsTagAssignment,
CMAssignmentReturn,
CMBeatsAdapter,
} from './adapters/beats/adapter_types';
import { FrontendDomainLibs } from './lib';
export class BeatsLib {
constructor(
private readonly adapter: CMBeatsAdapter,
private readonly libs: { tags: FrontendDomainLibs['tags'] }
) {}
public async get(id: string): Promise<CMPopulatedBeat | null> {
const beat = await this.adapter.get(id);
return beat ? (await this.mergeInTags([beat]))[0] : null;
}
public async getBeatWithToken(enrollmentToken: string): Promise<CMBeat | null> {
const beat = await this.adapter.getBeatWithToken(enrollmentToken);
return beat;
}
public async getBeatsWithTag(tagId: string): Promise<CMPopulatedBeat[]> {
const beats = await this.adapter.getBeatsWithTag(tagId);
return await this.mergeInTags(beats);
}
public async getAll(ESQuery?: any): Promise<CMPopulatedBeat[]> {
const beats = await this.adapter.getAll(ESQuery);
return await this.mergeInTags(beats);
}
public async update(id: string, beatData: Partial<CMBeat>): Promise<boolean> {
return await this.adapter.update(id, beatData);
}
public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise<BeatsRemovalReturn[]> {
return await this.adapter.removeTagsFromBeats(removals);
}
public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise<CMAssignmentReturn[]> {
return await this.adapter.assignTagsToBeats(assignments);
}
private async mergeInTags(beats: CMBeat[]): Promise<CMPopulatedBeat[]> {
const tagIds = flatten(beats.map(b => b.tags || []));
const tags = await this.libs.tags.getTagsWithIds(tagIds);
// TODO the filter should not be needed, if the data gets into a bad state, we should error
// and inform the user they need to delte the tag, or else we should auto delete it
const mergedBeats: CMPopulatedBeat[] = beats.map(
b =>
({
...b,
full_tags: (b.tags || []).map(tagId => tags.find(t => t.id === tagId)).filter(t => t),
} as CMPopulatedBeat)
);
return mergedBeats;
}
}

View file

@ -0,0 +1,67 @@
/*
* 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.
*/
// @ts-ignore
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
import { management } from 'ui/management';
// @ts-ignore: path dynamic for kibana
import { uiModules } from 'ui/modules';
// @ts-ignore
import { Notifier } from 'ui/notify';
// @ts-ignore: path dynamic for kibana
import routes from 'ui/routes';
import { INDEX_NAMES } from '../../../common/constants/index_names';
import { supportedConfigs } from '../../config_schemas';
import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter';
import { RestElasticsearchAdapter } from '../adapters/elasticsearch/rest';
import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter';
import { AxiosRestAPIAdapter } from '../adapters/rest_api/axios_rest_api_adapter';
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';
export function compose(): FrontendLibs {
const api = new AxiosRestAPIAdapter(chrome.getXsrfToken(), chrome.getBasePath());
const esAdapter = new RestElasticsearchAdapter(api, INDEX_NAMES.BEATS);
const tags = new TagsLib(new RestTagsAdapter(api), supportedConfigs);
const tokens = new RestTokensAdapter(api);
const beats = new BeatsLib(new RestBeatsAdapter(api), {
tags,
});
const domainLibs: FrontendDomainLibs = {
tags,
tokens,
beats,
};
const pluginUIModule = uiModules.get('app/beats_management');
const framework = new KibanaFrameworkAdapter(
pluginUIModule,
management,
routes,
chrome,
XPackInfoProvider,
Notifier
);
const libs: FrontendLibs = {
framework,
elasticsearch: new ElasticsearchLib(esAdapter),
...domainLibs,
};
return libs;
}

View file

@ -0,0 +1,64 @@
/*
* 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 'ui/autoload/all';
// @ts-ignore: path dynamic for kibana
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';
// @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 { supportedConfigs } from '../../config_schemas';
import { TagsLib } from '../tags';
import { MemoryElasticsearchAdapter } from './../adapters/elasticsearch/memory';
import { ElasticsearchLib } from './../elasticsearch';
export function compose(
mockIsKueryValid: (kuery: string) => boolean,
mockKueryToEsQuery: (kuery: string) => string,
suggestions: AutocompleteSuggestion[]
): FrontendLibs {
const esAdapter = new MemoryElasticsearchAdapter(
mockIsKueryValid,
mockKueryToEsQuery,
suggestions
);
const tags = new TagsLib(new MemoryTagsAdapter([]), supportedConfigs);
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,
null
);
const libs: FrontendLibs = {
...domainLibs,
elasticsearch: new ElasticsearchLib(esAdapter),
framework,
};
return libs;
}

View file

@ -0,0 +1,69 @@
/*
* 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 { AutocompleteSuggestion } from 'ui/autocomplete_providers';
import { ElasticsearchAdapter } from './adapters/elasticsearch/adapter_types';
interface HiddenFields {
op: 'is' | 'startsWith' | 'withoutPrefix';
value: string;
}
export class ElasticsearchLib {
private readonly hiddenFields: HiddenFields[] = [
{ op: 'startsWith', value: 'enrollment_token' },
{ op: 'is', value: 'beat.active' },
{ op: 'is', value: 'beat.enrollment_token' },
{ op: 'is', value: 'beat.access_token' },
{ op: 'is', value: 'beat.ephemeral_id' },
{ op: 'is', value: 'beat.verified_on' },
];
constructor(private readonly adapter: ElasticsearchAdapter) {}
public isKueryValid(kuery: string): boolean {
return this.adapter.isKueryValid(kuery);
}
public async convertKueryToEsQuery(kuery: string): Promise<string> {
return await this.adapter.convertKueryToEsQuery(kuery);
}
public async getSuggestions(
kuery: string,
selectionStart: any,
fieldPrefix?: string
): Promise<AutocompleteSuggestion[]> {
const suggestions = await this.adapter.getSuggestions(kuery, selectionStart);
const filteredSuggestions = suggestions.filter(suggestion => {
const hiddenFieldsCheck = this.hiddenFields;
if (fieldPrefix) {
hiddenFieldsCheck.push({
op: 'withoutPrefix',
value: `${fieldPrefix}.`,
});
}
return hiddenFieldsCheck.reduce((isvalid, field) => {
if (!isvalid) {
return false;
}
switch (field.op) {
case 'startsWith':
return !suggestion.text.startsWith(field.value);
case 'is':
return suggestion.text.trim() !== field.value;
case 'withoutPrefix':
return suggestion.text.startsWith(field.value);
}
}, true);
});
return filteredSuggestions;
}
}

View file

@ -0,0 +1,88 @@
/*
* 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 { IModule, IScope } from 'angular';
import { AxiosRequestConfig } from 'axios';
import React from 'react';
import { CMTokensAdapter } from './adapters/tokens/adapter_types';
import { BeatsLib } from './beats';
import { ElasticsearchLib } from './elasticsearch';
import { TagsLib } from './tags';
export interface FrontendDomainLibs {
beats: BeatsLib;
tags: TagsLib;
tokens: CMTokensAdapter;
}
export interface FrontendLibs extends FrontendDomainLibs {
elasticsearch: ElasticsearchLib;
framework: FrameworkAdapter;
}
export interface YamlConfigSchema {
id: string;
ui: {
label: string;
type: 'input' | 'multi-input' | 'select' | 'code' | 'password';
helpText?: string;
transform?: 'removed';
};
options?: Array<{ value: string; text: string }>;
validations?: 'isHosts' | 'isString' | 'isPeriod' | 'isPath' | 'isPaths' | 'isYaml';
error: string;
defaultValue?: string;
required?: boolean;
parseValidResult?: (value: any) => any;
}
export interface FrameworkAdapter {
// Instance vars
appState?: object;
kbnVersion?: string;
baseURLPath: string;
registerManagementSection(pluginId: string, displayName: string, basePath: string): void;
// Methods
setUISettings(key: string, value: any): void;
render(component: React.ReactElement<any>): void;
}
export interface FramworkAdapterConstructable {
new (uiModule: IModule): FrameworkAdapter;
}
// TODO: replace AxiosRequestConfig with something more defined
export type RequestConfig = AxiosRequestConfig;
export interface ApiAdapter {
kbnVersion: string;
get<T>(url: string, config?: RequestConfig | undefined): Promise<T>;
post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise<object>;
delete(url: string, config?: RequestConfig | undefined): Promise<object>;
put(url: string, data?: any, config?: RequestConfig | undefined): Promise<object>;
}
export interface UiKibanaAdapterScope extends IScope {
breadcrumbs: any[];
topNavMenu: any[];
}
export interface KibanaUIConfig {
get(key: string): any;
set(key: string, value: any): Promise<boolean>;
}
export interface KibanaAdapterServiceRefs {
config: KibanaUIConfig;
rootScope: IScope;
}
export type BufferedKibanaServiceCall<ServiceRefs> = (serviceRefs: ServiceRefs) => void;
export interface Chrome {
setRootTemplate(template: string): void;
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import yaml from 'js-yaml';
import { omit, pick } from 'lodash';
import { BeatTag, ConfigurationBlock } from '../../common/domain_types';
import { ConfigContent } from '../../common/domain_types';
import { CMTagsAdapter } from './adapters/tags/adapter_types';
export class TagsLib {
constructor(private readonly adapter: CMTagsAdapter, private readonly tagConfigs: any) {}
public async getTagsWithIds(tagIds: string[]): Promise<BeatTag[]> {
return this.jsonConfigToUserYaml(await this.adapter.getTagsWithIds(tagIds));
}
public async delete(tagIds: string[]): Promise<boolean> {
return await this.adapter.delete(tagIds);
}
public async getAll(): Promise<BeatTag[]> {
return this.jsonConfigToUserYaml(await this.adapter.getAll());
}
public async upsertTag(tag: BeatTag): Promise<BeatTag | null> {
tag.id = tag.id.replace(' ', '-');
return await this.adapter.upsertTag(this.userConfigsToJson([tag])[0]);
}
public jsonConfigToUserYaml(tags: BeatTag[]): BeatTag[] {
return tags.map(tag => {
const transformedTag: BeatTag = tag as any;
// configuration_blocks yaml, JS cant read YAML so we parse it into JS,
// because beats flattens all fields, and we need more structure.
// we take tagConfigs, grab the config that applies here, render what we can into
// an object, and the rest we assume to be the yaml string that goes
// into the yaml editor...
// NOTE: The perk of this, is that as we support more features via controls
// vs yaml editing, it should "just work", and things that were in YAML
// will now be in the UI forms...
transformedTag.configuration_blocks = (tag.configuration_blocks || []).map(block => {
const { type, description, configs } = block;
const activeConfig = configs[0];
const thisConfig = this.tagConfigs.find((conf: any) => conf.value === type).config;
const knownConfigIds = thisConfig.map((config: any) => config.id);
const convertedConfig = knownConfigIds.reduce((blockObj: any, id: keyof ConfigContent) => {
blockObj[id] =
id === 'other' ? yaml.dump(omit(activeConfig, knownConfigIds)) : activeConfig[id];
return blockObj;
}, {});
// Workaround to empty object passed into dump resulting in this odd output
if (convertedConfig.other && convertedConfig.other === '{}\n') {
convertedConfig.other = '';
}
return {
type,
description,
configs: [convertedConfig],
} as ConfigurationBlock;
});
return transformedTag;
});
}
public userConfigsToJson(tags: BeatTag[]): BeatTag[] {
return tags.map(tag => {
const transformedTag: BeatTag = tag as any;
// configurations is the JS representation of the config yaml,
// so here we take that JS and convert it into a YAML string.
// we do so while also flattening "other" into the flat yaml beats expect
transformedTag.configuration_blocks = (tag.configuration_blocks || []).map(block => {
const { type, description, configs } = block;
const activeConfig = configs[0];
const thisConfig = this.tagConfigs.find((conf: any) => conf.value === type).config;
const knownConfigIds = thisConfig
.map((config: any) => config.id)
.filter((id: string) => id !== 'other');
const convertedConfig = {
...yaml.safeLoad(activeConfig.other),
...pick(activeConfig, knownConfigIds),
};
return {
type,
description,
configs: [convertedConfig],
} as ConfigurationBlock;
});
return transformedTag;
});
}
}

View file

@ -0,0 +1,13 @@
/*
* 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 from 'react';
export class NotFoundPage extends React.PureComponent {
public render() {
return <div>No content found</div>;
}
}

View file

@ -0,0 +1,58 @@
/*
* 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 { first, sortByOrder } from 'lodash';
import moment from 'moment';
import React from 'react';
import { CMPopulatedBeat } from '../../../common/domain_types';
interface BeatDetailsActionSectionProps {
beat: CMPopulatedBeat | undefined;
}
export const BeatDetailsActionSection = ({ beat }: BeatDetailsActionSectionProps) => (
<div>
{beat ? (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="xs">
Type:&nbsp;
<strong>{beat.type}</strong>.
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
Version:&nbsp;
<strong>{beat.version}</strong>.
</EuiText>
</EuiFlexItem>
{/* TODO: We need a populated field before we can run this code
<EuiFlexItem grow={false}>
<EuiText size="xs">
Uptime: <strong>12min.</strong>
</EuiText>
</EuiFlexItem> */}
{beat.full_tags &&
beat.full_tags.length > 0 && (
<EuiFlexItem grow={false}>
<EuiText size="xs">
Last Config Update:{' '}
<strong>
{moment(
first(sortByOrder(beat.full_tags, 'last_updated')).last_updated
).fromNow()}
</strong>
.
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
) : (
<div>Beat not found</div>
)}
</div>
);

View file

@ -0,0 +1,14 @@
/*
* 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 from 'react';
import { FrontendLibs } from '../../lib/lib';
interface BeatActivityPageProps {
libs: FrontendLibs;
}
export const BeatActivityPage = (props: BeatActivityPageProps) => <div>Beat Activity View</div>;

View file

@ -0,0 +1,103 @@
/*
* 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,
// @ts-ignore EuiInMemoryTable typings not yet available
EuiInMemoryTable,
EuiLink,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { flatten, get } from 'lodash';
import React from 'react';
import { TABLE_CONFIG } from '../../../common/constants';
import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types';
import { ConnectedLink } from '../../components/connected_link';
import { TagBadge } from '../../components/tag';
import { supportedConfigs } from '../../config_schemas';
interface BeatDetailPageProps {
beat: CMPopulatedBeat | undefined;
}
export const BeatDetailPage = (props: BeatDetailPageProps) => {
const { beat } = props;
if (!beat) {
return <div>Beat not found</div>;
}
const configurationBlocks = flatten(
beat.full_tags.map((tag: BeatTag) => {
return tag.configuration_blocks.map(configuration => ({
// @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property
module: configuration.configs[0].module || null,
tagId: tag.id,
tagColor: tag.color,
...beat,
...configuration,
displayValue: get(
supportedConfigs.find(config => config.value === configuration.type),
'text',
null
),
}));
})
);
const columns = [
{
field: 'displayValue',
name: 'Type',
sortable: true,
render: (value: string | null, configuration: any) => (
<EuiLink href="#">{value || configuration.type}</EuiLink>
),
},
{
field: 'module',
name: 'Module',
sortable: true,
},
{
field: 'description',
name: 'Description',
sortable: true,
},
{
field: 'tagId',
name: 'Tag',
render: (id: string, block: any) => (
<ConnectedLink path={`/tag/edit/${id}`}>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
tag={{ color: block.tagColor, id }}
/>
</ConnectedLink>
),
sortable: true,
},
];
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h4>Configurations</h4>
</EuiTitle>
<EuiText size="s">
<p>
You can have multiple configurations applied to an individual tag. These configurations
can repeat or mix types as necessary. For example, you may utilize three metricbeat
configurations alongside one input and filebeat configuration.
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiInMemoryTable columns={columns} items={configurationBlocks} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,157 @@
/*
* 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 {
EuiSpacer,
// @ts-ignore types for EuiTab not currently available
EuiTab,
// @ts-ignore types for EuiTabs not currently available
EuiTabs,
} from '@elastic/eui';
import React from 'react';
import { 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';
interface Match {
params: any;
}
interface BeatDetailsPageProps extends URLStateProps<AppURLState> {
location: any;
history: any;
libs: FrontendLibs;
match: Match;
}
interface BeatDetailsPageState {
beat: CMPopulatedBeat | undefined;
beatId: string;
isLoading: boolean;
}
class BeatDetailsPageComponent extends React.PureComponent<
BeatDetailsPageProps,
BeatDetailsPageState
> {
constructor(props: BeatDetailsPageProps) {
super(props);
this.state = {
beat: undefined,
beatId: this.props.match.params.beatId,
isLoading: true,
};
this.loadBeat();
}
public onSelectedTabChanged = (id: string) => {
this.props.history.push({
pathname: id,
search: this.props.location.search,
});
};
public render() {
const { beat } = this.state;
let id;
let name;
if (beat) {
id = beat.id;
name = beat.name;
}
const title = this.state.isLoading
? 'Loading'
: `Beat: ${name || 'No name receved from beat'} (id: ${id})`;
const tabs = [
{
id: `/beat/${id}`,
name: 'Config',
disabled: false,
},
// {
// id: `/beat/${id}/activity`,
// name: 'Beat Activity',
// disabled: false,
// },
{
id: `/beat/${id}/tags`,
name: 'Configuration Tags',
disabled: false,
},
];
return (
<PrimaryLayout title={title} actionSection={<BeatDetailsActionSection beat={beat} />}>
<EuiTabs>
{tabs.map((tab, index) => (
<EuiTab
disabled={tab.disabled}
key={index}
isSelected={tab.id === this.props.history.location.pathname}
onClick={() => {
this.props.history.push({
pathname: tab.id,
search: this.props.location.search,
});
}}
>
{tab.name}
</EuiTab>
))}
</EuiTabs>
<EuiSpacer size="l" />
<Switch>
<Route
path="/beat/:beatId/activity"
render={(props: any) => <BeatActivityPage libs={this.props.libs} {...props} />}
/>
<Route
path="/beat/:beatId/tags"
render={(props: any) => (
<BeatTagsPage
beatId={this.state.beatId}
libs={this.props.libs}
refreshBeat={() => this.loadBeat()}
{...props}
/>
)}
/>
<Route
path="/beat/:beatId"
render={(props: any) => (
<BeatDetailPage beat={this.state.beat} libs={this.props.libs} {...props} />
)}
/>
</Switch>
</PrimaryLayout>
);
}
private async loadBeat() {
const { beatId } = this.props.match.params;
let beat;
try {
beat = await this.props.libs.beats.get(beatId);
if (!beat) {
throw new Error('beat not found');
}
} catch (e) {
throw new Error(e);
}
this.setState({ beat, isLoading: false });
}
}
export const BeatDetailsPage = withUrlState<BeatDetailsPageProps>(BeatDetailsPageComponent);

View file

@ -0,0 +1,67 @@
/*
* 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 { EuiGlobalToastList } from '@elastic/eui';
import React from 'react';
import { CMPopulatedBeat } from '../../../common/domain_types';
import { BeatDetailTagsTable, Table } from '../../components/table';
import { FrontendLibs } from '../../lib/lib';
interface BeatTagsPageProps {
beatId: string;
libs: FrontendLibs;
refreshBeat(): void;
}
interface BeatTagsPageState {
beat: CMPopulatedBeat | null;
notifications: any[];
}
export class BeatTagsPage extends React.PureComponent<BeatTagsPageProps, BeatTagsPageState> {
private tableRef = React.createRef<Table>();
constructor(props: BeatTagsPageProps) {
super(props);
this.state = {
beat: null,
notifications: [],
};
}
public async componentWillMount() {
await this.getBeat();
}
public render() {
const { beat } = this.state;
return (
<div>
<Table
hideTableControls={true}
items={beat ? beat.full_tags : []}
ref={this.tableRef}
type={BeatDetailTagsTable}
/>
<EuiGlobalToastList
toasts={this.state.notifications}
dismissToast={() => this.setState({ notifications: [] })}
toastLifeTimeMs={5000}
/>
</div>
);
}
private getBeat = async () => {
try {
const beat = await this.props.libs.beats.get(this.props.beatId);
this.setState({ beat });
} catch (e) {
throw new Error(e);
}
};
}

View file

@ -0,0 +1,13 @@
/*
* 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 from 'react';
export class ActivityPage extends React.PureComponent {
public render() {
return <div>activity logs view</div>;
}
}

View file

@ -0,0 +1,292 @@
/*
* 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,
EuiButtonEmpty,
EuiGlobalToastList,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
} from '@elastic/eui';
import { sortBy } from 'lodash';
import moment from 'moment';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { CMPopulatedBeat } from '../../../common/domain_types';
import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types';
import { AppURLState } from '../../app';
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';
interface BeatsPageProps extends URLStateProps<AppURLState> {
libs: FrontendLibs;
location: any;
beats: CMPopulatedBeat[];
loadBeats: () => any;
}
interface BeatsPageState {
notifications: any[];
tableRef: any;
tags: any[] | null;
}
interface ActionAreaProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
libs: FrontendLibs;
}
export class BeatsPage extends React.PureComponent<BeatsPageProps, BeatsPageState> {
public static ActionArea = (props: ActionAreaProps) => (
<React.Fragment>
<EuiButtonEmpty
onClick={() => {
// random, but specific number ensures new tab does not overwrite another _newtab in chrome
// and at the same time not truly random so that many clicks of the link open many tabs at this same URL
window.open(
'https://www.elastic.co/guide/en/beats/libbeat/current/getting-started.html',
'_newtab35628937456'
);
}}
>
Learn how to install beats
</EuiButtonEmpty>
<EuiButton
size="s"
color="primary"
onClick={async () => {
props.goTo(`/overview/beats/enroll`);
}}
>
Enroll Beats
</EuiButton>
{props.location.pathname === '/overview/beats/enroll' && (
<EuiOverlayMask>
<EuiModal
onClose={() => {
props.goTo(`/overview/beats`);
}}
style={{ width: '640px' }}
>
<EuiModalHeader>
<EuiModalHeaderTitle>Enroll a new Beat</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EnrollBeatPage {...props} />
</EuiModalBody>
</EuiModal>
</EuiOverlayMask>
)}
</React.Fragment>
);
constructor(props: BeatsPageProps) {
super(props);
this.state = {
notifications: [],
tableRef: React.createRef(),
tags: null,
};
}
public componentDidUpdate(prevProps: any) {
if (this.props.location !== prevProps.location) {
this.props.loadBeats();
}
}
public render() {
return (
<div>
<WithKueryAutocompletion libs={this.props.libs} fieldPrefix="beat">
{autocompleteProps => (
<Table
kueryBarProps={{
...autocompleteProps,
filterQueryDraft: 'false', // todo
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
onSubmit: () => null, // todo
value: this.props.urlState.beatsKBar || '',
}}
assignmentOptions={{
items: this.state.tags || [],
schema: beatsListAssignmentOptions,
type: 'assignment',
actionHandler: this.handleBeatsActions,
}}
items={sortBy(this.props.beats, 'id') || []}
ref={this.state.tableRef}
type={BeatsTableType}
/>
)}
</WithKueryAutocompletion>
<EuiGlobalToastList
toasts={this.state.notifications}
dismissToast={() => this.setState({ notifications: [] })}
toastLifeTimeMs={5000}
/>
</div>
);
}
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.Search:
this.handleSearchQuery(payload);
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.notifyBeatDisenrolled(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);
};
// todo: add reference to ES filter endpoint
private handleSearchQuery = (query: any) => {
// await this.props.libs.beats.searach(query);
};
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 notifyBeatDisenrolled = async (beats: CMPopulatedBeat[]) => {
let title;
let text;
if (beats.length === 1) {
title = `"${beats[0].name || beats[0].id}" disenrolled`;
text = `Beat with ID "${beats[0].id}" was disenrolled.`;
} else {
title = `${beats.length} beats disenrolled`;
}
this.setState({
notifications: this.state.notifications.concat({
color: 'warning',
id: `disenroll_${new Date()}`,
title,
text,
}),
});
};
private notifyUpdatedTagAssociation = (
action: 'add' | 'remove',
assignments: BeatsTagAssignment[],
tag: string
) => {
const actionName = action === 'remove' ? 'Removed' : 'Added';
const preposition = action === 'remove' ? 'from' : 'to';
const beatMessage =
assignments.length && assignments.length === 1
? `beat "${this.getNameForBeatId(assignments[0].beatId)}"`
: `${assignments.length} beats`;
this.setState({
notifications: this.state.notifications.concat({
color: 'success',
id: `tag-${moment.now()}`,
text: <p>{`${actionName} tag "${tag}" ${preposition} ${beatMessage}.`}</p>,
title: `Tag ${actionName}`,
}),
});
};
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);
const beats: CMPopulatedBeat[] = [];
selectedIds.forEach((id: any) => {
const beat: CMPopulatedBeat | undefined = this.props.beats.find(b => b.id === id);
if (beat) {
beats.push(beat);
}
});
return beats;
};
}

View file

@ -0,0 +1,108 @@
/*
* 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, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import 'brace/mode/yaml';
import 'brace/theme/github';
import React from 'react';
import { BeatTag } from '../../../common/domain_types';
import { AppURLState } from '../../app';
import { TagEdit } from '../../components/tag';
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
import { FrontendLibs } from '../../lib/lib';
interface TagPageProps extends URLStateProps<AppURLState> {
libs: FrontendLibs;
match: any;
}
interface TagPageState {
showFlyout: boolean;
tag: BeatTag;
}
export class CreateTagFragment extends React.PureComponent<TagPageProps, TagPageState> {
private mode: 'edit' | 'create' = 'create';
constructor(props: TagPageProps) {
super(props);
this.state = {
showFlyout: false,
tag: {
id: props.urlState.createdTag ? props.urlState.createdTag : '',
color: '#DD0A73',
configuration_blocks: [],
last_updated: new Date(),
},
};
if (props.urlState.createdTag) {
this.mode = 'edit';
this.loadTag();
}
}
public render() {
return (
<React.Fragment>
<TagEdit
tag={this.state.tag}
mode={this.mode}
onDetachBeat={(beatIds: string[]) => {
this.props.libs.beats.removeTagsFromBeats(
beatIds.map(id => {
return { beatId: id, tag: this.state.tag.id };
})
);
}}
onTagChange={(field: string, value: string | number) =>
this.setState(oldState => ({
tag: { ...oldState.tag, [field]: value },
}))
}
attachedBeats={null}
/>
<EuiSpacer />
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={
this.state.tag.id === '' || this.state.tag.configuration_blocks.length === 0
}
onClick={this.saveTag}
>
Save & Continue
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
);
}
private loadTag = async () => {
const tags = await this.props.libs.tags.getTagsWithIds([this.state.tag.id]);
if (tags.length > 0) {
this.setState({
tag: tags[0],
});
}
};
private saveTag = async () => {
const newTag = await this.props.libs.tags.upsertTag(this.state.tag as BeatTag);
if (!newTag) {
return alert('error saving tag');
}
this.props.setUrlState({
createdTag: newTag.id,
});
this.props.goTo(`/overview/initial/review`);
};
}
export const CreateTagPageFragment = withUrlState<TagPageProps>(CreateTagFragment);

View file

@ -0,0 +1,280 @@
/*
* 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 typings for EuiBasicTable not present in current version
EuiBasicTable,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiModalBody,
// @ts-ignore
EuiSelect,
EuiTitle,
} from '@elastic/eui';
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';
interface BeatsProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
match: any;
libs: FrontendLibs;
}
export class EnrollBeat extends React.Component<BeatsProps, any> {
private pinging = false;
constructor(props: BeatsProps) {
super(props);
this.state = {
enrolledBeat: null,
command: 'sudo filebeat',
beatType: 'filebeat',
};
}
public pingForBeatWithToken = async (
libs: FrontendLibs,
token: string
): Promise<CMBeat | void> => {
try {
const beats = await libs.beats.getBeatWithToken(token);
if (!beats) {
throw new Error('no beats');
}
return beats;
} catch (err) {
if (this.pinging) {
const timeout = (ms: number) => new Promise(res => setTimeout(res, ms));
await timeout(5000);
return await this.pingForBeatWithToken(libs, token);
}
}
};
public async componentDidMount() {
if (!this.props.urlState.enrollmentToken) {
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
this.props.setUrlState({
enrollmentToken,
});
}
}
public waitForToken = async (token: string) => {
if (this.pinging) {
return;
}
this.pinging = true;
const enrolledBeat = (await this.pingForBeatWithToken(this.props.libs, token)) as CMBeat;
this.setState({
enrolledBeat,
});
this.pinging = false;
};
public render() {
if (!this.props.urlState.enrollmentToken) {
return null;
}
if (this.props.urlState.enrollmentToken && !this.state.enrolledBeat) {
this.waitForToken(this.props.urlState.enrollmentToken);
}
const { goTo } = this.props;
const actions = [];
switch (this.props.location.pathname) {
case '/overview/initial/beats':
actions.push({
goTo: '/overview/initial/tag',
name: 'Continue',
});
break;
case '/overview/beats/enroll':
actions.push({
goTo: '/overview/beats/enroll',
name: 'Enroll another Beat',
newToken: true,
});
actions.push({
goTo: '/overview/beats',
name: 'Done',
clearToken: true,
});
break;
}
return (
<React.Fragment>
{!this.state.enrolledBeat && (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Select your beat type:</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSelect
value={this.state.beatType}
options={[
{ value: 'filebeat', label: 'Filebeat' },
{ value: 'metricbeat', label: 'Metricbeat' },
]}
onChange={(e: any) => this.setState({ beatType: e.target.value })}
fullWidth={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
<br />
<br />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Select your operating system:</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSelect
value={this.state.command}
options={[
{
value: `sudo ${this.state.beatType}`,
label: 'DEB',
},
{
value: `PS C:\\Program Files\\${capitalize(this.state.beatType)}> ${
this.state.beatType
}.exe`,
label: 'Windows',
},
{
value: `./${this.state.beatType}`,
label: 'MacOS',
},
{
value: `sudo ${this.state.beatType}`,
label: 'RPM',
},
]}
onChange={(e: any) => this.setState({ command: e.target.value })}
fullWidth={true}
/>
</EuiFlexItem>
</EuiFlexGroup>
<br />
<br />
{this.state.command && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Run the following command to enroll your beat</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<div className="euiFormControlLayout euiFormControlLayout--fullWidth">
<div
className="euiFieldText euiFieldText--fullWidth"
style={{ textAlign: 'left' }}
>
$ {this.state.command} enroll {window.location.protocol}
{`//`}
{window.location.host}
{this.props.libs.framework.baseURLPath
? this.props.libs.framework.baseURLPath
: ''}{' '}
{this.props.urlState.enrollmentToken}
</div>
</div>
<br />
<br />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Waiting for enroll command to be run...</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<br />
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
</EuiFlexGroup>
)}
</React.Fragment>
)}
{this.state.enrolledBeat && (
<EuiModalBody style={{ textAlign: 'center' }}>
A Beat was enrolled with the following data:
<br />
<br />
<br />
<EuiBasicTable
items={[this.state.enrolledBeat]}
columns={[
{
field: 'type',
name: 'Beat Type',
sortable: false,
},
{
field: 'version',
name: 'Version',
sortable: false,
},
{
field: 'host_name',
name: 'Hostname',
sortable: false,
},
]}
/>
<br />
<br />
{actions.map(action => (
<EuiButton
key={action.name}
size="s"
color="primary"
style={{ marginLeft: 10 }}
onClick={async () => {
if (action.clearToken) {
this.props.setUrlState({ enrollmentToken: '' });
}
if (action.newToken) {
const enrollmentToken = await this.props.libs.tokens.createEnrollmentToken();
this.props.setUrlState({ enrollmentToken });
return this.setState({
enrolledBeat: null,
});
}
goTo(action.goTo);
}}
>
{action.name}
</EuiButton>
))}
</EuiModalBody>
)}
</React.Fragment>
);
}
}
export const EnrollBeatPage = withUrlState<BeatsProps>(EnrollBeat);

View file

@ -0,0 +1,256 @@
/*
* 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 {
EuiCode,
// @ts-ignore
EuiTab,
// @ts-ignore
EuiTabs,
} from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
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 { 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 { TagsPage } from './tags';
import { ReviewWalkthroughPage } from './walkthrough_review';
interface MainPagesProps extends URLStateProps<AppURLState> {
libs: FrontendLibs;
location: any;
}
interface MainPagesState {
enrollBeat?: {
enrollmentToken: string;
} | null;
beats: CMPopulatedBeat[];
unfilteredBeats: CMPopulatedBeat[];
loadedBeatsAtLeastOnce: boolean;
}
class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesState> {
private mounted: boolean = false;
constructor(props: MainPagesProps) {
super(props);
this.state = {
loadedBeatsAtLeastOnce: false,
beats: [],
unfilteredBeats: [],
};
}
public onSelectedTabChanged = (id: string) => {
this.props.goTo(id);
};
public componentDidMount() {
this.mounted = true;
this.loadBeats();
}
public componentWillUnmount() {
this.mounted = false;
}
public render() {
if (
this.state.loadedBeatsAtLeastOnce &&
this.state.unfilteredBeats.length === 0 &&
!this.props.location.pathname.includes('/overview/initial')
) {
return <Redirect to="/overview/initial/help" />;
}
const tabs = [
{
id: '/overview/beats',
name: 'Beats List',
disabled: false,
},
// {
// id: '/overview/activity',
// name: 'Beats Activity',
// disabled: false,
// },
{
id: '/overview/tags',
name: 'Configuration Tags',
disabled: false,
},
];
const walkthroughSteps = [
{
id: '/overview/initial/beats',
name: 'Enroll Beat',
disabled: false,
page: EnrollBeatPage,
},
{
id: '/overview/initial/tag',
name: 'Create Configuration Tag',
disabled: false,
page: CreateTagPageFragment,
},
{
id: '/overview/initial/review',
name: 'Review',
disabled: false,
page: ReviewWalkthroughPage,
},
];
if (this.props.location.pathname === '/overview/initial/help') {
return (
<NoDataLayout
title="Welcome to Beats Central Management"
actionSection={
<ConnectedLink path="/overview/initial/beats">
<EuiButton color="primary" fill>
Enroll Beat
</EuiButton>
</ConnectedLink>
}
>
<p>
You dont have any Beat configured to use Central Management, click on{' '}
<EuiCode>Enroll Beat</EuiCode> to add one now.
</p>
</NoDataLayout>
);
}
if (this.props.location.pathname.includes('/overview/initial')) {
return (
<WalkthroughLayout
title="Get Started With Beats Centeral Management"
walkthroughSteps={walkthroughSteps}
goTo={this.props.goTo}
activePath={this.props.location.pathname}
>
<Switch>
{walkthroughSteps.map(step => (
<Route
path={step.id}
render={(props: any) => (
<step.page
{...this.props}
{...props}
libs={this.props.libs}
loadBeats={this.loadBeats}
/>
)}
/>
))}
</Switch>
</WalkthroughLayout>
);
}
const renderedTabs = tabs.map((tab, index) => (
<EuiTab
onClick={() => this.onSelectedTabChanged(tab.id)}
isSelected={tab.id === this.props.location.pathname}
disabled={tab.disabled}
key={index}
>
{tab.name}
</EuiTab>
));
return (
<PrimaryLayout
title="Beats"
actionSection={
<Switch>
<Route
path="/overview/beats/:action?/:enrollmentToken?"
render={(props: any) => (
<BeatsPage.ActionArea {...this.props} {...props} libs={this.props.libs} />
)}
/>
<Route
path="/overview/tags"
render={(props: any) => (
<TagsPage.ActionArea {...this.props} {...props} libs={this.props.libs} />
)}
/>
</Switch>
}
>
<EuiTabs>{renderedTabs}</EuiTabs>
<Switch>
<Route
path="/overview/beats/:action?/:enrollmentToken?"
render={(props: any) => (
<BeatsPage
{...this.props}
libs={this.props.libs}
{...props}
loadBeats={this.loadBeats}
beats={this.state.beats}
/>
)}
/>
<Route
path="/overview/activity"
exact={true}
render={(props: any) => (
<ActivityPage {...this.props} libs={this.props.libs} {...props} />
)}
/>
<Route
path="/overview/tags"
exact={true}
render={(props: any) => <TagsPage {...this.props} libs={this.props.libs} {...props} />}
/>
</Switch>
</PrimaryLayout>
);
}
private loadBeats = async () => {
let query;
if (this.props.urlState.beatsKBar) {
query = await this.props.libs.elasticsearch.convertKueryToEsQuery(
this.props.urlState.beatsKBar
);
}
let beats: CMPopulatedBeat[];
let unfilteredBeats: CMPopulatedBeat[];
try {
[beats, unfilteredBeats] = await Promise.all([
this.props.libs.beats.getAll(query),
this.props.libs.beats.getAll(),
]);
} catch (e) {
beats = [];
unfilteredBeats = [];
}
if (this.mounted) {
this.setState({
loadedBeatsAtLeastOnce: true,
beats,
unfilteredBeats,
});
}
};
}
export const MainPages = withUrlState<MainPagesProps>(MainPagesComponent);

View file

@ -0,0 +1,77 @@
/*
* 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 from 'react';
import { BeatTag } from '../../../common/domain_types';
import { AppURLState } from '../../app';
import { Table, TagsTableType } from '../../components/table';
import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion';
import { URLStateProps } from '../../containers/with_url_state';
import { FrontendLibs } from '../../lib/lib';
interface TagsPageProps extends URLStateProps<AppURLState> {
libs: FrontendLibs;
}
interface TagsPageState {
tags: BeatTag[];
}
export class TagsPage extends React.PureComponent<TagsPageProps, TagsPageState> {
public static ActionArea = ({ goTo }: TagsPageProps) => (
<EuiButton
size="s"
color="primary"
onClick={async () => {
goTo('/tag/create');
}}
>
Add Tag
</EuiButton>
);
constructor(props: TagsPageProps) {
super(props);
this.state = {
tags: [],
};
this.loadTags();
}
public render() {
return (
<WithKueryAutocompletion libs={this.props.libs} fieldPrefix="tag">
{autocompleteProps => (
<Table
kueryBarProps={{
...autocompleteProps,
filterQueryDraft: 'false', // todo
isValid: this.props.libs.elasticsearch.isKueryValid(
this.props.urlState.tagsKBar || ''
),
onChange: (value: any) => this.props.setUrlState({ tagsKBar: value }),
onSubmit: () => null, // todo
value: this.props.urlState.tagsKBar || '',
}}
hideTableControls={true}
items={this.state.tags}
type={TagsTableType}
/>
)}
</WithKueryAutocompletion>
);
}
private async loadTags() {
const tags = await this.props.libs.tags.getAll();
this.setState({
tags,
});
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { BeatTag, CMBeat } from '../../../common/domain_types';
import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types';
import { AppURLState } from '../../app';
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
import { FrontendLibs } from '../../lib/lib';
interface PageProps extends URLStateProps<AppURLState>, RouteComponentProps<any> {
loadBeats: any;
libs: FrontendLibs;
}
export class ReviewWalkthrough extends React.Component<PageProps, any> {
constructor(props: PageProps) {
super(props);
this.state = {
assigned: false,
};
}
public componentDidMount() {
setTimeout(async () => {
await this.props.loadBeats();
const done = await this.assignTagToBeat();
if (done) {
this.setState({
assigned: true,
});
}
}, 300);
}
public render() {
const { goTo } = this.props;
return (
<EuiFlexGroup justifyContent="spaceAround" style={{ marginTop: 50 }}>
<EuiFlexItem grow={false}>
<EuiPageContent>
<EuiEmptyPrompt
iconType="logoBeats"
title={<h2>Congratulations!</h2>}
body={
<React.Fragment>
<p>
You have enrolled your first beat, and we have assigned your new tag with its
configurations to it
</p>
<h3>Next Steps</h3>
<p>All that is left to do is to start the beat you just enrolled.</p>
</React.Fragment>
}
actions={
<EuiButton
fill
disabled={!this.state.assigned}
onClick={async () => {
goTo('/overview/beats');
}}
>
Done
</EuiButton>
}
/>
</EuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
);
}
private createBeatTagAssignments = (beats: CMBeat[], tag: BeatTag): BeatsTagAssignment[] =>
beats.map(({ id }) => ({ beatId: id, tag: tag.id }));
private assignTagToBeat = async () => {
if (!this.props.urlState.enrollmentToken) {
return alert('Invalid URL, no enrollmentToken found');
}
if (!this.props.urlState.createdTag) {
return alert('Invalid URL, no createdTag found');
}
const beat = await this.props.libs.beats.getBeatWithToken(this.props.urlState.enrollmentToken);
if (!beat) {
return alert('Error: Beat not enrolled properly');
}
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);
this.props.setUrlState({
createdTag: '',
enrollmentToken: '',
});
return true;
};
}
export const ReviewWalkthroughPage = withUrlState<PageProps>(ReviewWalkthrough);

View file

@ -0,0 +1,121 @@
/*
* 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, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import 'brace/mode/yaml';
import 'brace/theme/github';
import React from 'react';
import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types';
import { AppURLState } from '../../app';
import { PrimaryLayout } from '../../components/layouts/primary';
import { TagEdit } from '../../components/tag';
import { URLStateProps, withUrlState } from '../../containers/with_url_state';
import { FrontendLibs } from '../../lib/lib';
interface TagPageProps extends URLStateProps<AppURLState> {
libs: FrontendLibs;
match: any;
}
interface TagPageState {
showFlyout: boolean;
attachedBeats: CMPopulatedBeat[] | null;
tag: BeatTag;
}
export class TagPageComponent extends React.PureComponent<TagPageProps, TagPageState> {
private mode: 'edit' | 'create' = 'create';
constructor(props: TagPageProps) {
super(props);
this.state = {
showFlyout: false,
attachedBeats: null,
tag: {
id: props.match.params.action === 'create' ? '' : props.match.params.tagid,
color: '#DD0A73',
configuration_blocks: [],
last_updated: new Date(),
},
};
if (props.match.params.action !== 'create') {
this.mode = 'edit';
this.loadTag();
this.loadAttachedBeats();
}
}
public render() {
return (
<PrimaryLayout
title={this.mode === 'create' ? 'Create Tag' : `Update Tag: ${this.state.tag.id}`}
>
<div>
<TagEdit
tag={this.state.tag}
mode={this.mode}
onDetachBeat={async (beatIds: string[]) => {
await this.props.libs.beats.removeTagsFromBeats(
beatIds.map(id => {
return { beatId: id, tag: this.state.tag.id };
})
);
await this.loadAttachedBeats();
}}
onTagChange={(field: string, value: string | number) =>
this.setState(oldState => ({
tag: { ...oldState.tag, [field]: value },
}))
}
attachedBeats={this.state.attachedBeats}
/>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
disabled={
this.state.tag.id === '' // || this.state.tag.configuration_blocks.length === 0
}
onClick={this.saveTag}
>
Save
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => this.props.goTo('/overview/tags')}>
Cancel
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</PrimaryLayout>
);
}
private loadTag = async () => {
const tags = await this.props.libs.tags.getTagsWithIds([this.props.match.params.tagid]);
if (tags.length === 0) {
// TODO do something to error
}
this.setState({
tag: tags[0],
});
};
private loadAttachedBeats = async () => {
const beats = await this.props.libs.beats.getBeatsWithTag(this.props.match.params.tagid);
this.setState({
attachedBeats: beats,
});
};
private saveTag = async () => {
await this.props.libs.tags.upsertTag(this.state.tag as BeatTag);
this.props.goTo(`/overview/tags`);
};
}
export const TagPage = withUrlState<TagPageProps>(TagPageComponent);

View file

@ -0,0 +1,51 @@
/*
* 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 from 'react';
import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
import { Header } from './components/layouts/header';
import { FrontendLibs } from './lib/lib';
import { BeatDetailsPage } from './pages/beat';
import { MainPages } from './pages/main';
import { TagPage } from './pages/tag';
export const PageRouter: React.SFC<{ libs: FrontendLibs }> = ({ libs }) => {
return (
<HashRouter basename="/management/beats_management">
<div>
<Header
breadcrumbs={[
{
href: '#/management',
text: 'Management',
},
{
href: '#/management/beats_management',
text: 'Beats',
},
]}
/>
<Switch>
<Route
path="/"
exact={true}
render={() => <Redirect from="/" exact={true} to="/overview/beats" />}
/>
<Route path="/overview" render={(props: any) => <MainPages {...props} libs={libs} />} />
<Route
path="/beat/:beatId"
render={(props: any) => <BeatDetailsPage {...props} libs={libs} />}
/>
<Route
path="/tag/:action/:tagid?"
render={(props: any) => <TagPage {...props} libs={libs} />}
/>
</Switch>
</div>
</HashRouter>
);
};

View file

@ -0,0 +1,65 @@
/*
* 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 { omit } from 'lodash';
import React from 'react';
import { InferableComponentEnhancerWithProps } from 'react-redux';
export type RendererResult = React.ReactElement<any> | null;
export type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result;
export type ChildFunctionRendererProps<RenderArgs> = {
children: RendererFunction<RenderArgs>;
initializeOnMount?: boolean;
resetOnUnmount?: boolean;
} & RenderArgs;
interface ChildFunctionRendererOptions<RenderArgs> {
onInitialize?: (props: RenderArgs) => void;
onCleanup?: (props: RenderArgs) => void;
}
export const asChildFunctionRenderer = <InjectedProps, OwnProps>(
hoc: InferableComponentEnhancerWithProps<InjectedProps, OwnProps>,
{ onInitialize, onCleanup }: ChildFunctionRendererOptions<InjectedProps> = {}
) =>
hoc(
class ChildFunctionRenderer extends React.Component<ChildFunctionRendererProps<InjectedProps>> {
public displayName = 'ChildFunctionRenderer';
public componentDidMount() {
if (this.props.initializeOnMount && onInitialize) {
onInitialize(this.getRendererArgs());
}
}
public componentWillUnmount() {
if (this.props.resetOnUnmount && onCleanup) {
onCleanup(this.getRendererArgs());
}
}
public render() {
return this.props.children(this.getRendererArgs());
}
private getRendererArgs = () =>
omit(['children', 'initializeOnMount', 'resetOnUnmount'], this.props) as Pick<
ChildFunctionRendererProps<InjectedProps>,
keyof InjectedProps
>;
}
);
export type StateUpdater<State, Props = {}> = (
prevState: Readonly<State>,
prevProps: Readonly<Props>
) => State | null;
export function composeStateUpdaters<State, Props>(...updaters: Array<StateUpdater<State, Props>>) {
return (state: State, props: Props) =>
updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state);
}

View file

@ -0,0 +1,22 @@
# Documentation for Beats CM in x-pack kibana
Notes:
Falure to have auth enabled in Kibana will make for a broken UI. UI based errors not yet in place
### Run tests
```
node scripts/jest.js plugins/beats --watch
```
and for functional... (from x-pack root)
```
node scripts/functional_tests --config test/api_integration/config
```
### Run command to fake an enrolling beat (from beats_management dir)
```
node scripts/enroll.js <enrollment token>
```

View file

@ -0,0 +1,35 @@
/*
* 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.
*/
const request = require('request');
const Chance = require('chance'); // eslint-disable-line
const args = process.argv.slice(2);
const chance = new Chance();
const enroll = async token => {
const beatId = chance.word();
await request(
{
url: `http://localhost:5601/api/beats/agent/${beatId}`,
method: 'POST',
headers: {
'kbn-xsrf': 'xxx',
'kbn-beats-enrollment-token': token,
},
body: JSON.stringify({
type: 'filebeat',
host_name: `${chance.word()}.bar.com`,
name: chance.word(),
version: '6.3.0',
}),
},
(error, response, body) => {
console.log(error, body);
}
);
};
enroll(args[0]);

View file

@ -0,0 +1,13 @@
/*
* 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 { compose } from './lib/compose/kibana';
import { initManagementServer } from './management_server';
export const initServerWithKibana = (hapiServer: any) => {
const libs = compose(hapiServer);
initManagementServer(libs);
};

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CMBeat } from '../../../../common/domain_types';
import { FrameworkUser } from '../framework/adapter_types';
export interface CMBeatsAdapter {
insert(user: FrameworkUser, beat: CMBeat): Promise<void>;
update(user: FrameworkUser, beat: CMBeat): Promise<void>;
get(user: FrameworkUser, id: string): Promise<CMBeat | null>;
getAll(user: FrameworkUser, ESQuery?: any): Promise<CMBeat[]>;
getWithIds(user: FrameworkUser, beatIds: string[]): Promise<CMBeat[]>;
getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise<CMBeat[]>;
getBeatWithToken(user: FrameworkUser, enrollmentToken: string): Promise<CMBeat | null>;
removeTagsFromBeats(
user: FrameworkUser,
removals: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]>;
assignTagsToBeats(
user: FrameworkUser,
assignments: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]>;
}
export interface BeatsTagAssignment {
beatId: string;
tag: string;
idxInRequest?: number;
}
interface BeatsReturnedTagAssignment {
status: number | null;
result?: string;
}
export interface CMAssignmentReturn {
assignments: BeatsReturnedTagAssignment[];
}
export interface BeatsRemovalReturn {
removals: BeatsReturnedTagAssignment[];
}

View file

@ -0,0 +1,239 @@
/*
* 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 { 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';
export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
private database: DatabaseAdapter;
constructor(database: DatabaseAdapter) {
this.database = database;
}
public async get(user: FrameworkUser, id: string) {
const params = {
id: `beat:${id}`,
ignore: [404],
index: INDEX_NAMES.BEATS,
type: '_doc',
};
const response = await this.database.get(user, params);
if (!response.found) {
return null;
}
return _get<CMBeat>(response, '_source.beat');
}
public async insert(user: FrameworkUser, beat: CMBeat) {
const body = {
beat,
type: 'beat',
};
await this.database.index(user, {
body,
id: `beat:${beat.id}`,
index: INDEX_NAMES.BEATS,
refresh: 'wait_for',
type: '_doc',
});
}
public async update(user: FrameworkUser, beat: CMBeat) {
const body = {
beat,
type: 'beat',
};
const params = {
body,
id: `beat:${beat.id}`,
index: INDEX_NAMES.BEATS,
refresh: 'wait_for',
type: '_doc',
};
await this.database.index(user, params);
}
public async getWithIds(user: FrameworkUser, beatIds: string[]) {
const ids = beatIds.map(beatId => `beat:${beatId}`);
const params = {
_sourceInclude: ['beat.id', 'beat.verified_on'],
body: {
ids,
},
index: INDEX_NAMES.BEATS,
type: '_doc',
};
const response = await this.database.mget(user, params);
return _get(response, 'docs', [])
.filter((b: any) => b.found)
.map((b: any) => b._source.beat);
}
public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise<CMBeat[]> {
const params = {
ignore: [404],
index: INDEX_NAMES.BEATS,
type: '_doc',
body: {
query: {
terms: { 'beat.tags': tagIds },
},
},
};
const response = await this.database.search(user, params);
const beats = _get<CMBeat[]>(response, 'hits.hits', []);
if (beats.length === 0) {
return [];
}
return beats.map((beat: any) => omit(beat._source.beat, ['access_token']));
}
public async getBeatWithToken(
user: FrameworkUser,
enrollmentToken: string
): Promise<CMBeat | null> {
const params = {
ignore: [404],
index: INDEX_NAMES.BEATS,
type: '_doc',
body: {
query: {
match: { 'beat.enrollment_token': enrollmentToken },
},
},
};
const response = await this.database.search(user, params);
const beats = _get<CMBeat[]>(response, 'hits.hits', []);
if (beats.length === 0) {
return null;
}
return omit<CMBeat, {}>(_get<CMBeat>(beats[0], '_source.beat'), ['access_token']);
}
public async getAll(user: FrameworkUser, ESQuery?: any) {
const params = {
index: INDEX_NAMES.BEATS,
size: 10000,
type: '_doc',
body: {
query: {
bool: {
must: {
term: {
type: 'beat',
},
},
},
},
},
};
if (ESQuery) {
params.body.query = {
...params.body.query,
...ESQuery,
};
}
let response;
try {
response = await this.database.search(user, params);
} catch (e) {
// TODO something
}
if (!response) {
return [];
}
const beats = _get<any>(response, 'hits.hits', []);
return beats.map((beat: any) => omit(beat._source.beat, ['access_token']));
}
public async removeTagsFromBeats(
user: FrameworkUser,
removals: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]> {
const body = flatten(
removals.map(({ beatId, tag }) => {
const script = `
def beat = ctx._source.beat;
if (beat.tags != null) {
beat.tags.removeAll([params.tag]);
}`;
return [
{ update: { _id: `beat:${beatId}` } },
{ script: { source: script.replace(' ', ''), params: { tag } } },
];
})
);
const response = await this.database.bulk(user, {
body,
index: INDEX_NAMES.BEATS,
refresh: 'wait_for',
type: '_doc',
});
return _get<any>(response, 'items', []).map((item: any, resultIdx: number) => ({
idxInRequest: removals[resultIdx].idxInRequest,
result: item.update.result,
status: item.update.status,
}));
}
public async assignTagsToBeats(
user: FrameworkUser,
assignments: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]> {
const body = flatten(
assignments.map(({ beatId, tag }) => {
const script = `
def beat = ctx._source.beat;
if (beat.tags == null) {
beat.tags = [];
}
if (!beat.tags.contains(params.tag)) {
beat.tags.add(params.tag);
}`;
return [
{ update: { _id: `beat:${beatId}` } },
{ script: { source: script.replace(' ', ''), params: { tag } } },
];
})
);
const response = await this.database.bulk(user, {
body,
index: INDEX_NAMES.BEATS,
refresh: 'wait_for',
type: '_doc',
});
return _get<any>(response, 'items', []).map((item: any, resultIdx: any) => ({
idxInRequest: assignments[resultIdx].idxInRequest,
result: item.update.result,
status: item.update.status,
}));
}
}

View file

@ -0,0 +1,114 @@
/*
* 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 { intersection, omit } from 'lodash';
import { CMBeat } from '../../../../common/domain_types';
import { FrameworkUser } from '../framework/adapter_types';
import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types';
export class MemoryBeatsAdapter implements CMBeatsAdapter {
private beatsDB: CMBeat[];
constructor(beatsDB: CMBeat[]) {
this.beatsDB = beatsDB;
}
public async get(user: FrameworkUser, id: string) {
return this.beatsDB.find(beat => beat.id === id) || null;
}
public async insert(user: FrameworkUser, beat: CMBeat) {
this.beatsDB.push(beat);
}
public async update(user: FrameworkUser, beat: CMBeat) {
const beatIndex = this.beatsDB.findIndex(b => b.id === beat.id);
this.beatsDB[beatIndex] = {
...this.beatsDB[beatIndex],
...beat,
};
}
public async getWithIds(user: FrameworkUser, beatIds: string[]) {
return this.beatsDB.filter(beat => beatIds.includes(beat.id));
}
public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise<CMBeat[]> {
return this.beatsDB.filter(beat => intersection(tagIds, beat.tags || []).length !== 0);
}
public async getBeatWithToken(
user: FrameworkUser,
enrollmentToken: string
): Promise<CMBeat | null> {
return this.beatsDB.find(beat => enrollmentToken === beat.enrollment_token) || null;
}
public async getAll(user: FrameworkUser) {
return this.beatsDB.map<CMBeat>((beat: any) => omit(beat, ['access_token']));
}
public async removeTagsFromBeats(
user: FrameworkUser,
removals: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]> {
const beatIds = removals.map(r => r.beatId);
const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => {
const tagData = removals.find(r => r.beatId === beat.id);
if (tagData) {
if (beat.tags) {
beat.tags = beat.tags.filter(tag => tag !== tagData.tag);
}
}
return beat;
});
return response.map<any>((item: CMBeat, resultIdx: number) => ({
idxInRequest: removals[resultIdx].idxInRequest,
result: 'updated',
status: 200,
}));
}
public async assignTagsToBeats(
user: FrameworkUser,
assignments: BeatsTagAssignment[]
): Promise<BeatsTagAssignment[]> {
const beatIds = assignments.map(r => r.beatId);
this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => {
// get tags that need to be assigned to this beat
const tags = assignments
.filter(a => a.beatId === beat.id)
.map((t: BeatsTagAssignment) => t.tag);
if (tags.length > 0) {
if (!beat.tags) {
beat.tags = [];
}
const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t));
if (nonExistingTags.length > 0) {
beat.tags = beat.tags.concat(nonExistingTags);
}
}
return beat;
});
return assignments.map<any>((item: BeatsTagAssignment, resultIdx: number) => ({
idxInRequest: assignments[resultIdx].idxInRequest,
result: 'updated',
status: 200,
}));
}
public setDB(beatsDB: CMBeat[]) {
this.beatsDB = beatsDB;
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.
*/
// file.skip
// @ts-ignore
import { createEsTestCluster } from '@kbn/test';
import { Root } from 'src/core/server/root';
// @ts-ignore
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
import { DatabaseKbnESPlugin } from '../adapter_types';
import { KibanaDatabaseAdapter } from '../kibana_database_adapter';
import { contractTests } from './test_contract';
const es = createEsTestCluster({});
let legacyServer: any;
let rootServer: Root;
contractTests('Kibana Database Adapter', {
before: async () => {
await es.start();
rootServer = kbnTestServer.createRootWithCorePlugins({
server: { maxPayloadBytes: 100 },
});
await rootServer.start();
legacyServer = kbnTestServer.getKbnServer(rootServer);
return await legacyServer.plugins.elasticsearch.waitUntilReady();
},
after: async () => {
await rootServer.shutdown();
return await es.cleanup();
},
adapterSetup: () => {
return new KibanaDatabaseAdapter(legacyServer.plugins.elasticsearch as DatabaseKbnESPlugin);
},
});

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { beatsIndexTemplate } from '../../../../utils/index_templates';
import { DatabaseAdapter } from '../adapter_types';
interface ContractConfig {
before?(): Promise<void>;
after?(): Promise<void>;
adapterSetup(): DatabaseAdapter;
}
export const contractTests = (testName: string, config: ContractConfig) => {
describe.skip(testName, () => {
let database: DatabaseAdapter;
beforeAll(async () => {
jest.setTimeout(100000); // 1 second
if (config.before) {
await config.before();
}
});
afterAll(async () => config.after && (await config.after()));
beforeEach(async () => {
database = config.adapterSetup();
});
it('Should inject template into ES', async () => {
try {
await database.putTemplate(
{ kind: 'internal' },
{
name: 'beats-template',
body: beatsIndexTemplate,
}
);
} catch (e) {
expect(e).toEqual(null);
}
});
it('Unauthorized users cant query', async () => {
const params = {
id: `beat:foo`,
ignore: [404],
index: '.management-beats',
type: '_doc',
};
let ranWithoutError = false;
try {
await database.get({ kind: 'unauthenticated' }, params);
ranWithoutError = true;
} catch (e) {
expect(e).not.toEqual(null);
}
expect(ranWithoutError).toEqual(false);
});
it('Should query ES', async () => {
const params = {
id: `beat:foo`,
ignore: [404],
index: '.management-beats',
type: '_doc',
};
const response = await database.get({ kind: 'internal' }, params);
expect(response).not.toEqual(undefined);
// @ts-ignore
expect(response.found).toEqual(undefined);
});
});
};

View file

@ -0,0 +1,307 @@
/*
* 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 { FrameworkRequest, FrameworkUser } from '../framework/adapter_types';
export interface DatabaseAdapter {
putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise<any>;
get<Source>(
user: FrameworkUser,
params: DatabaseGetParams
): Promise<DatabaseGetDocumentResponse<Source>>;
create(
user: FrameworkUser,
params: DatabaseCreateDocumentParams
): Promise<DatabaseCreateDocumentResponse>;
index<T>(
user: FrameworkUser,
params: DatabaseIndexDocumentParams<T>
): Promise<DatabaseIndexDocumentResponse>;
delete(
user: FrameworkUser,
params: DatabaseDeleteDocumentParams
): Promise<DatabaseDeleteDocumentResponse>;
mget<T>(user: FrameworkUser, params: DatabaseMGetParams): Promise<DatabaseMGetResponse<T>>;
bulk(
user: FrameworkUser,
params: DatabaseBulkIndexDocumentsParams
): Promise<DatabaseBulkResponse>;
search<T>(user: FrameworkUser, params: DatabaseSearchParams): Promise<DatabaseSearchResponse<T>>;
}
export interface DatabaseKbnESCluster {
callWithInternalUser(esMethod: string, options: {}): Promise<any>;
callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise<any>;
}
export interface DatabaseKbnESPlugin {
getCluster(clusterName: string): DatabaseKbnESCluster;
}
export interface DatabaseSearchParams extends DatabaseGenericParams {
analyzer?: string;
analyzeWildcard?: boolean;
defaultOperator?: DefaultOperator;
df?: string;
explain?: boolean;
storedFields?: DatabaseNameList;
docvalueFields?: DatabaseNameList;
fielddataFields?: DatabaseNameList;
from?: number;
ignoreUnavailable?: boolean;
allowNoIndices?: boolean;
expandWildcards?: ExpandWildcards;
lenient?: boolean;
lowercaseExpandedTerms?: boolean;
preference?: string;
q?: string;
routing?: DatabaseNameList;
scroll?: string;
searchType?: 'query_then_fetch' | 'dfs_query_then_fetch';
size?: number;
sort?: DatabaseNameList;
_source?: DatabaseNameList;
_sourceExclude?: DatabaseNameList;
_sourceInclude?: DatabaseNameList;
terminateAfter?: number;
stats?: DatabaseNameList;
suggestField?: string;
suggestMode?: 'missing' | 'popular' | 'always';
suggestSize?: number;
suggestText?: string;
timeout?: string;
trackScores?: boolean;
version?: boolean;
requestCache?: boolean;
index?: DatabaseNameList;
type?: DatabaseNameList;
}
export interface DatabaseSearchResponse<T> {
took: number;
timed_out: boolean;
_scroll_id?: string;
_shards: DatabaseShardsResponse;
hits: {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: DatabaseExplanation;
fields?: any;
highlight?: any;
inner_hits?: any;
sort?: string[];
}>;
};
aggregations?: any;
}
export interface DatabaseExplanation {
value: number;
description: string;
details: DatabaseExplanation[];
}
export interface DatabaseShardsResponse {
total: number;
successful: number;
failed: number;
skipped: number;
}
export interface DatabaseGetDocumentResponse<Source> {
_index: string;
_type: string;
_id: string;
_version: number;
found: boolean;
_source: Source;
}
export interface DatabaseBulkResponse {
took: number;
errors: boolean;
items: Array<
DatabaseDeleteDocumentResponse | DatabaseIndexDocumentResponse | DatabaseUpdateDocumentResponse
>;
}
export interface DatabaseBulkIndexDocumentsParams extends DatabaseGenericParams {
waitForActiveShards?: string;
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
type?: string;
fields?: DatabaseNameList;
_source?: DatabaseNameList;
_sourceExclude?: DatabaseNameList;
_sourceInclude?: DatabaseNameList;
pipeline?: string;
index?: string;
}
export interface DatabaseMGetParams extends DatabaseGenericParams {
storedFields?: DatabaseNameList;
preference?: string;
realtime?: boolean;
refresh?: boolean;
_source?: DatabaseNameList;
_sourceExclude?: DatabaseNameList;
_sourceInclude?: DatabaseNameList;
index: string;
type?: string;
}
export interface DatabaseMGetResponse<T> {
docs?: Array<DatabaseGetResponse<T>>;
}
export interface DatabasePutTemplateParams extends DatabaseGenericParams {
name: string;
body: any;
}
export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams {
waitForActiveShards?: string;
parent?: string;
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
version?: number;
versionType?: DatabaseVersionType;
index: string;
type: string;
id: string;
}
export interface DatabaseIndexDocumentResponse {
found: boolean;
_index: string;
_type: string;
_id: string;
_version: number;
result: string;
}
export interface DatabaseUpdateDocumentResponse {
found: boolean;
_index: string;
_type: string;
_id: string;
_version: number;
result: string;
}
export interface DatabaseDeleteDocumentResponse {
found: boolean;
_index: string;
_type: string;
_id: string;
_version: number;
result: string;
}
export interface DatabaseIndexDocumentParams<T> extends DatabaseGenericParams {
waitForActiveShards?: string;
opType?: 'index' | 'create';
parent?: string;
refresh?: string;
routing?: string;
timeout?: string;
timestamp?: Date | number;
ttl?: string;
version?: number;
versionType?: DatabaseVersionType;
pipeline?: string;
id?: string;
index: string;
type: string;
body: T;
}
export interface DatabaseGetResponse<T> {
found: boolean;
_source: T;
}
export interface DatabaseCreateDocumentParams extends DatabaseGenericParams {
waitForActiveShards?: string;
parent?: string;
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
timestamp?: Date | number;
ttl?: string;
version?: number;
versionType?: DatabaseVersionType;
pipeline?: string;
id?: string;
index: string;
type: string;
}
export interface DatabaseCreateDocumentResponse {
created: boolean;
result: string;
}
export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams {
waitForActiveShards?: string;
parent?: string;
refresh?: DatabaseRefresh;
routing?: string;
timeout?: string;
version?: number;
versionType?: DatabaseVersionType;
index: string;
type: string;
id: string;
}
export interface DatabaseGetParams extends DatabaseGenericParams {
storedFields?: DatabaseNameList;
parent?: string;
preference?: string;
realtime?: boolean;
refresh?: boolean;
routing?: string;
_source?: DatabaseNameList;
_sourceExclude?: DatabaseNameList;
_sourceInclude?: DatabaseNameList;
version?: number;
versionType?: DatabaseVersionType;
id: string;
index: string;
type: string;
}
export type DatabaseNameList = string | string[] | boolean;
export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | '';
export type DatabaseVersionType = 'internal' | 'external' | 'external_gte' | 'force';
export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all';
export type DefaultOperator = 'AND' | 'OR';
export type DatabaseConflicts = 'abort' | 'proceed';
export interface DatabaseGenericParams {
requestTimeout?: number;
maxRetries?: number;
method?: string;
body?: any;
ignore?: number | number[];
filterPath?: string | string[];
}
export interface DatabaseDeleteDocumentResponse {
found: boolean;
_index: string;
_type: string;
_id: string;
_version: number;
result: string;
}

View file

@ -0,0 +1,107 @@
/*
* 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 { internalAuthData } from '../../../utils/wrap_request';
import { FrameworkUser } from '../framework/adapter_types';
import {
DatabaseAdapter,
DatabaseBulkIndexDocumentsParams,
DatabaseCreateDocumentParams,
DatabaseCreateDocumentResponse,
DatabaseDeleteDocumentParams,
DatabaseDeleteDocumentResponse,
DatabaseGetDocumentResponse,
DatabaseGetParams,
DatabaseIndexDocumentParams,
DatabaseKbnESCluster,
DatabaseKbnESPlugin,
DatabaseMGetParams,
DatabaseMGetResponse,
DatabasePutTemplateParams,
DatabaseSearchParams,
DatabaseSearchResponse,
} from './adapter_types';
export class KibanaDatabaseAdapter implements DatabaseAdapter {
private es: DatabaseKbnESCluster;
constructor(kbnElasticSearch: DatabaseKbnESPlugin) {
this.es = kbnElasticSearch.getCluster('admin');
}
public async putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise<any> {
const callES = this.getCallType(user);
const result = await callES('indices.putTemplate', params);
return result;
}
public async get<Source>(
user: FrameworkUser,
params: DatabaseGetParams
): Promise<DatabaseGetDocumentResponse<Source>> {
const callES = this.getCallType(user);
const result = await callES('get', params);
return result;
// todo
}
public async mget<T>(
user: FrameworkUser,
params: DatabaseMGetParams
): Promise<DatabaseMGetResponse<T>> {
const callES = this.getCallType(user);
const result = await callES('mget', params);
return result;
// todo
}
public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise<any> {
const callES = this.getCallType(user);
const result = await callES('bulk', params);
return result;
}
public async create(
user: FrameworkUser,
params: DatabaseCreateDocumentParams
): Promise<DatabaseCreateDocumentResponse> {
const callES = this.getCallType(user);
const result = await callES('create', params);
return result;
}
public async index<T>(user: FrameworkUser, params: DatabaseIndexDocumentParams<T>): Promise<any> {
const callES = this.getCallType(user);
const result = await callES('index', params);
return result;
}
public async delete(
user: FrameworkUser,
params: DatabaseDeleteDocumentParams
): Promise<DatabaseDeleteDocumentResponse> {
const callES = this.getCallType(user);
const result = await callES('delete', params);
return result;
}
public async search<Source>(
user: FrameworkUser,
params: DatabaseSearchParams
): Promise<DatabaseSearchResponse<Source>> {
const callES = this.getCallType(user);
const result = await callES('search', params);
return result;
}
private getCallType(user: FrameworkUser): any {
if (user.kind === 'authenticated') {
return this.es.callWithRequest.bind(null, {
headers: user[internalAuthData],
});
} else if (user.kind === 'internal') {
return this.es.callWithInternalUser;
} else {
throw new Error('Invalid user type');
}
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 { FrameworkUser } from '../framework/adapter_types';
import {
DatabaseAdapter,
DatabaseBulkIndexDocumentsParams,
DatabaseCreateDocumentParams,
DatabaseCreateDocumentResponse,
DatabaseDeleteDocumentParams,
DatabaseDeleteDocumentResponse,
DatabaseGetDocumentResponse,
DatabaseGetParams,
DatabaseIndexDocumentParams,
DatabaseMGetParams,
DatabaseMGetResponse,
DatabasePutTemplateParams,
DatabaseSearchParams,
DatabaseSearchResponse,
} from './adapter_types';
export class MemoryDatabaseAdapter implements DatabaseAdapter {
public async putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise<any> {
return null;
}
public async get<Source>(
user: FrameworkUser,
params: DatabaseGetParams
): Promise<DatabaseGetDocumentResponse<Source>> {
throw new Error('get not implamented in memory');
}
public async mget<T>(
user: FrameworkUser,
params: DatabaseMGetParams
): Promise<DatabaseMGetResponse<T>> {
throw new Error('mget not implamented in memory');
}
public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise<any> {
throw new Error('mget not implamented in memory');
}
public async create(
user: FrameworkUser,
params: DatabaseCreateDocumentParams
): Promise<DatabaseCreateDocumentResponse> {
throw new Error('create not implamented in memory');
}
public async index<T>(user: FrameworkUser, params: DatabaseIndexDocumentParams<T>): Promise<any> {
throw new Error('index not implamented in memory');
}
public async delete(
user: FrameworkUser,
params: DatabaseDeleteDocumentParams
): Promise<DatabaseDeleteDocumentResponse> {
throw new Error('delete not implamented in memory');
}
public async search<Source>(
user: FrameworkUser,
params: DatabaseSearchParams
): Promise<DatabaseSearchResponse<Source>> {
throw new Error('search not implamented in memory');
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// file.skip
// @ts-ignore
import { createEsTestCluster } from '@kbn/test';
import { config as beatsPluginConfig, configPrefix } from '../../../../..';
// @ts-ignore
import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server';
import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter';
import { contractTests } from './test_contract';
const kbnServer = kbnTestServer.createRootWithCorePlugins({ server: { maxPayloadBytes: 100 } });
const legacyServer = kbnTestServer.getKbnServer(kbnServer);
contractTests('Kibana Framework Adapter', {
before: async () => {
await kbnServer.start();
const config = legacyServer.server.config();
config.extendSchema(beatsPluginConfig, {}, configPrefix);
config.set('xpack.beats.encryptionKey', 'foo');
},
after: async () => {
await kbnServer.shutdown();
},
adapterSetup: () => {
return new KibanaBackendFrameworkAdapter(legacyServer.server);
},
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { BackendFrameworkAdapter } from '../adapter_types';
interface ContractConfig {
before?(): Promise<void>;
after?(): Promise<void>;
adapterSetup(): BackendFrameworkAdapter;
}
export const contractTests = (testName: string, config: ContractConfig) => {
describe.skip(testName, () => {
// let frameworkAdapter: BackendFrameworkAdapter;
beforeAll(async () => {
jest.setTimeout(100000); // 1 second
if (config.before) {
await config.before();
}
});
afterAll(async () => config.after && (await config.after()));
beforeEach(async () => {
// FIXME: one of these always should exist, type ContractConfig as such
// const frameworkAdapter = config.adapterSetup();
});
it('Should have tests here', () => {
expect(true).toEqual(true);
});
});
};

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { internalAuthData } from '../../../utils/wrap_request';
export interface BackendFrameworkAdapter {
internalUser: FrameworkInternalUser;
version: string;
getSetting(settingPath: string): any;
exposeStaticDir(urlPath: string, dir: string): void;
registerRoute<RouteRequest extends FrameworkWrappableRequest, RouteResponse>(
route: FrameworkRouteOptions<RouteRequest, RouteResponse>
): void;
}
export interface FrameworkAuthenticatedUser<AuthDataType = any> {
kind: 'authenticated';
[internalAuthData]: AuthDataType;
}
export interface FrameworkUnAuthenticatedUser {
kind: 'unauthenticated';
}
export interface FrameworkInternalUser {
kind: 'internal';
}
export type FrameworkUser<AuthDataType = any> =
| FrameworkAuthenticatedUser<AuthDataType>
| FrameworkUnAuthenticatedUser
| FrameworkInternalUser;
export interface FrameworkRequest<
InternalRequest extends FrameworkWrappableRequest = FrameworkWrappableRequest
> {
user: FrameworkUser<InternalRequest['headers']>;
headers: InternalRequest['headers'];
info: InternalRequest['info'];
payload: InternalRequest['payload'];
params: InternalRequest['params'];
query: InternalRequest['query'];
}
export interface FrameworkRouteOptions<
RouteRequest extends FrameworkWrappableRequest,
RouteResponse
> {
path: string;
method: string | string[];
vhost?: string;
licenseRequired?: boolean;
handler: FrameworkRouteHandler<RouteRequest, RouteResponse>;
config?: {};
}
export type FrameworkRouteHandler<RouteRequest extends FrameworkWrappableRequest, RouteResponse> = (
request: FrameworkRequest<RouteRequest>,
reply: any
) => void;
export interface FrameworkWrappableRequest<
Payload = any,
Params = any,
Query = any,
Headers = any,
Info = any
> {
headers: Headers;
info: Info;
payload: Payload;
params: Params;
query: Query;
}

Some files were not shown because too many files have changed in this diff Show more