mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* [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:
parent
44c89d9360
commit
d8f65387af
278 changed files with 13611 additions and 83 deletions
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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];
|
11
x-pack/plugins/beats_management/common/constants/index.ts
Normal file
11
x-pack/plugins/beats_management/common/constants/index.ts
Normal 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';
|
|
@ -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',
|
||||
};
|
|
@ -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',
|
||||
};
|
12
x-pack/plugins/beats_management/common/constants/table.ts
Normal file
12
x-pack/plugins/beats_management/common/constants/table.ts
Normal 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,
|
||||
};
|
87
x-pack/plugins/beats_management/common/domain_types.ts
Normal file
87
x-pack/plugins/beats_management/common/domain_types.ts
Normal 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;
|
||||
}
|
38
x-pack/plugins/beats_management/index.ts
Normal file
38
x-pack/plugins/beats_management/index.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
14
x-pack/plugins/beats_management/public/app.d.ts
vendored
Normal file
14
x-pack/plugins/beats_management/public/app.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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';
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
`;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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>;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
(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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
370
x-pack/plugins/beats_management/public/config_schemas.ts
Normal file
370
x-pack/plugins/beats_management/public/config_schemas.ts
Normal 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 },
|
||||
];
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
30
x-pack/plugins/beats_management/public/index.tsx
Normal file
30
x-pack/plugins/beats_management/public/index.tsx
Normal 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());
|
|
@ -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('');
|
||||
});
|
||||
});
|
|
@ -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[];
|
||||
}
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
70
x-pack/plugins/beats_management/public/lib/beats.ts
Normal file
70
x-pack/plugins/beats_management/public/lib/beats.ts
Normal 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;
|
||||
}
|
||||
}
|
67
x-pack/plugins/beats_management/public/lib/compose/kibana.ts
Normal file
67
x-pack/plugins/beats_management/public/lib/compose/kibana.ts
Normal 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;
|
||||
}
|
64
x-pack/plugins/beats_management/public/lib/compose/memory.ts
Normal file
64
x-pack/plugins/beats_management/public/lib/compose/memory.ts
Normal 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;
|
||||
}
|
69
x-pack/plugins/beats_management/public/lib/elasticsearch.ts
Normal file
69
x-pack/plugins/beats_management/public/lib/elasticsearch.ts
Normal 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;
|
||||
}
|
||||
}
|
88
x-pack/plugins/beats_management/public/lib/lib.ts
Normal file
88
x-pack/plugins/beats_management/public/lib/lib.ts
Normal 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;
|
||||
}
|
98
x-pack/plugins/beats_management/public/lib/tags.ts
Normal file
98
x-pack/plugins/beats_management/public/lib/tags.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import 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;
|
||||
});
|
||||
}
|
||||
}
|
13
x-pack/plugins/beats_management/public/pages/404.tsx
Normal file
13
x-pack/plugins/beats_management/public/pages/404.tsx
Normal 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>;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
<strong>{beat.type}</strong>.
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
Version:
|
||||
<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>
|
||||
);
|
|
@ -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>;
|
103
x-pack/plugins/beats_management/public/pages/beat/detail.tsx
Normal file
103
x-pack/plugins/beats_management/public/pages/beat/detail.tsx
Normal 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>
|
||||
);
|
||||
};
|
157
x-pack/plugins/beats_management/public/pages/beat/index.tsx
Normal file
157
x-pack/plugins/beats_management/public/pages/beat/index.tsx
Normal 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);
|
67
x-pack/plugins/beats_management/public/pages/beat/tags.tsx
Normal file
67
x-pack/plugins/beats_management/public/pages/beat/tags.tsx
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
292
x-pack/plugins/beats_management/public/pages/main/beats.tsx
Normal file
292
x-pack/plugins/beats_management/public/pages/main/beats.tsx
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
256
x-pack/plugins/beats_management/public/pages/main/index.tsx
Normal file
256
x-pack/plugins/beats_management/public/pages/main/index.tsx
Normal 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 don’t 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);
|
77
x-pack/plugins/beats_management/public/pages/main/tags.tsx
Normal file
77
x-pack/plugins/beats_management/public/pages/main/tags.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
121
x-pack/plugins/beats_management/public/pages/tag/index.tsx
Normal file
121
x-pack/plugins/beats_management/public/pages/tag/index.tsx
Normal 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);
|
51
x-pack/plugins/beats_management/public/router.tsx
Normal file
51
x-pack/plugins/beats_management/public/router.tsx
Normal 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>
|
||||
);
|
||||
};
|
65
x-pack/plugins/beats_management/public/utils/typed_react.ts
Normal file
65
x-pack/plugins/beats_management/public/utils/typed_react.ts
Normal 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);
|
||||
}
|
22
x-pack/plugins/beats_management/readme.md
Normal file
22
x-pack/plugins/beats_management/readme.md
Normal 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>
|
||||
```
|
35
x-pack/plugins/beats_management/scripts/enroll.js
Normal file
35
x-pack/plugins/beats_management/scripts/enroll.js
Normal 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]);
|
13
x-pack/plugins/beats_management/server/kibana.index.ts
Normal file
13
x-pack/plugins/beats_management/server/kibana.index.ts
Normal 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);
|
||||
};
|
|
@ -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[];
|
||||
}
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue