From 5f6a578a7a499ca25b1a07e4afaaed8990fe7fa2 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 29 Aug 2023 17:47:44 -0700 Subject: [PATCH] [Data Explorer] Merge to main (#4806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Squashed commit of the following: commit 551e4328a21ed204eaa96039f878118dc82ee249 Author: Ashwin P Chandran Date: Wed Aug 23 14:31:52 2023 -0700 [Data Explorer] Merge main conflicts (#4800) * [Saved Object Service] Adds Repository Factory Provider (#4149) * Adds Repository Factory Provider Signed-off-by: Bandini Bhopi * add category option for context menus (#4144) * enhance grouping for context menu options Signed-off-by: David Sinclair * change log Signed-off-by: David Sinclair * remove type export Signed-off-by: David Sinclair * revert border and prevent destroy options Signed-off-by: David Sinclair * update comments for building panels Signed-off-by: David Sinclair * build panels tests and more comments Signed-off-by: David Sinclair * add category option for context menus Signed-off-by: David Sinclair * changelog Signed-off-by: David Sinclair * add order to groups Signed-off-by: David Sinclair * documentation, shorter copyrighty, minor cleanup Signed-off-by: David Sinclair * changelog Signed-off-by: David Sinclair --------- Signed-off-by: David Sinclair Signed-off-by: David Sinclair Signed-off-by: Ashish Agrawal Co-authored-by: Ashish Agrawal * [CCI] Add bluebird replaces for src/plugins/saved_objects (#4026) * Add bluebird replaces for src/plugins/saved_objects * Add changelog entry --------- Signed-off-by: Alexei Karikov * Validate and correct change log after 2.8 release (#4275) Signed-off-by: Su * [DEVELOPER_GUIDE] resolving links (#3989) * links Signed-off-by: Aigerim Suleimenova * new section for doveloper guide Signed-off-by: Aigerim Suleimenova * updates Signed-off-by: Aigerim Suleimenova * Update DEVELOPER_GUIDE.md Co-authored-by: Ashwin P Chandran Signed-off-by: Aigerim Suleimenova * Update DEVELOPER_GUIDE.md Signed-off-by: Josh Romero --------- Signed-off-by: Aigerim Suleimenova Signed-off-by: Josh Romero Co-authored-by: Ashwin P Chandran Co-authored-by: Josh Romero * Enable data client with sample data server side (#4268) * Enable data client with sample data server side * Add dataSourceId into savedObject Signed-off-by: Kristen Tian * Functional list, install uninstall Signed-off-by: Kristen Tian * add change log Signed-off-by: Kristen Tian * address comments Signed-off-by: Kristen Tian * add ut Signed-off-by: Kristen Tian --------- Signed-off-by: Kristen Tian * Upgrade the backport workflow (#4343) * Copy over the labels from the original PR * Label the backport PR with `autocut` * Label a PR that fails to backport Signed-off-by: Miki * Hide any output from `use_node` checking for Node compatibility (#4237) Signed-off-by: Miki Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Vis Colors] Update default color in TSVB to use `ouiPaletteColorBlind()[0]` (#4363) Signed-off-by: Manasvini B Suryanarayana * Add BWC tests for 2.7 and 2.8 (#4023) Signed-off-by: Manasvini B Suryanarayana * [Vis colors] Replace vis_type_timeline colors with `ouiPaletteColorBlind()` (#4366) Signed-off-by: Manasvini B Suryanarayana * [Lint] add custom stylelint rules and config (#4290) * [Lint] add custom stylelint rules and config Adding `@osd/stylelint-config` and `@osd/stylelint-plugin-stylelint` packages. These packages are utilized by OSD core and can be ran with the following: `yarn lint:style` Can be used to fix known non-compliant styling with the following: `yarn lint:style --fix` Can be used to audit untracked styling (based on defined rules) with the following: ``` export OUI_AUDIT_ENABLED=true yarn lint:style ``` --- `@osd/stylelint-config` Defines rules approved by UX and OSD core in JSON files and is added to OSD core. Within this commit is defined `colors.json` and `global_selectors.json`. `colors.json` defines a property that can be matched with a regex of a selector. If the selector is tracked it will have an `approved` value and a list of `rejected` values that UX knows if a value should be something. `global_selectors.json` defines a selector that if tracked, it will have an `approved` list of relative paths to files that can modify the global selector. --- `@osd/stylelint-plugin-stylelint` Creates the functionality that utilizes the JSON files within the `@osd/stylelint-config`. Within this commit is defined `no_custom_colors` and `no_modifying_global_selectors` rules. `no_custom_colors` checks if a property is a color property. It then utilizes a compliance engine helper to check the `colors.json` to see if the property being modified has a compliance rule available for the property for the specific selector and if it is not compliant. For example, if a selector matches `button` and we are trying to apply `background-color: red` to it. Stylelint will catch this and flag this as a known non-compliance issue since it knows that it should `$euiColorWarning`. If we pass `--fix` the property will be updated to be `$euiColorWarning`. If `OUI_AUDIT_ENABLED` is true it will catch all `background-color` being modified that is not being defined explicitly in `colors.json` `no_modifying_global_selectors` checks if a selector being modified is defined in `global_selectors.json` to see if a selector not defined in a specific list of approved files. For example, if a selector matches `#opensearch-dashboards-body` and it is being modified in `src/core/public/rendering/_base.scss`. Stylelint will catch this and flag this as a non-compliance issue. Since no other file should be modifying this selector. If we pass `--fix` the styling will be complete removed from the non-compliant file. --- Next steps: * Migrate these packages to OUI * Consider adding `yarn lint:style --fix` to the build release script here: https://github.com/opensearch-project/opensearch-build/blob/main/scripts/default/opensearch-dashboards/build.sh#L89 Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4246 Signed-off-by: Kawika Avilla * fix to use find Signed-off-by: Kawika Avilla * Add regex matching and OUI modification lint Signed-off-by: Matt Provost * add changelog Signed-off-by: Josh Romero * address issues Signed-off-by: Kawika Avilla --------- Signed-off-by: Kawika Avilla Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Matt Provost Co-authored-by: Josh Romero * Fix linked deps resolution (#4342) Signed-off-by: Miki * Add configurable `defaults` to `uiSettings` (#4344) Also now: * `theme:darkMode` and `theme:version` can be configured via `defaults` * unauthenticated users are no longer forced to light mode Signed-off-by: Miki * Refactor hardcode color to use OUI in `maps_legacy` (#4294) * Refactor color to use OUI * Pull theme value from actual active theme * Update changelog --------- Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Anan Zhuang Co-authored-by: Josh Romero * Remove broken flot documentation link for Ruby API (#4384) * Remove broken documentation link for Ruby API https://apidock.com/ruby/Time/to_i is currently down for maintenance But we don't need this link anyway, because it's talking about standard methods. And we plan to deprecate flot_charts altogether: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4267 Signed-off-by: Josh Romero * remove leftover link brackets Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * [CCI] Fix relationships header overflow (#4070) * Fix relationships header overflow (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Replace relationships css file with oui classname (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Make title overflow wrap instead of truncation (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Remove icon from flyout header and wrap title Signed-off-by: Josh Romero --------- Signed-off-by: Sergey Myssak Signed-off-by: Josh Romero Co-authored-by: Andrey Myssak Co-authored-by: Josh Romero * Refactor color maps to use OUI color palettes (#4293) * Remove color_util Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Revert "Remove color_util" This reverts commit 9ca9c56e6bc5d2750971e04a2df7028f5c472b8b. Signed-off-by: Matt Provost * Refactor color maps to use Oui color palettes Signed-off-by: Matt Provost * Update changelog pt 2: electric boogaloo Signed-off-by: Matt Provost * Make gradients look better Signed-off-by: Matt Provost * Fix typescript ignore Signed-off-by: Matt Provost * Fix tests Signed-off-by: Matt Provost * Add todo followup Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [CCI] Remove unused tags in the navigation plugin (#3964) * Remove unused tags in the navigation plugin (#3962) Signed-off-by: Andrey Myssak * Update CHANGELOG.md (#3962) Signed-off-by: Andrey Myssak --------- Signed-off-by: Andrey Myssak Signed-off-by: Manasvini B Suryanarayana Co-authored-by: Manasvini B Suryanarayana * [Stylelint] Add invalid properties rule (#4374) * Add invalid properties rule Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Rename old variable Signed-off-by: Matt Provost * Add types for configs Signed-off-by: Matt Provost * Rename rule to no_restricted_properties Signed-off-by: Matt Provost * Refactor duplicate functions into generic one Signed-off-by: Matt Provost * Add type definitions Signed-off-by: Matt Provost * Add some documentation about supported config types Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Optchain instead of unwrapping source file Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost * Migrate from legacy elasticsearch client to opensearch-js client in `osd-opensearch-archiver` package (#4142) Signed-off-by: Manasvini B Suryanarayana Signed-off-by: Josh Romero Co-authored-by: Josh Romero * Chore (deps): Bump OUI to 1.1.2 to add anomoly detection icon (#4408) * Chore (deps): Bump OUI to 1.1.1 to add anomoly detection icon Signed-off-by: Josh Romero * update changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Excludes broken sass-lang link from Link checker (#4415) * fix: Link checker exclude Signed-off-by: Ashwin P Chandran * just ignore the broken URL for now Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran * [Vis Colors] Update legacy seed colors to use `ouiPaletteColorBlind()` (#4348) Signed-off-by: Manasvini B Suryanarayana * Release notes for 1.3.11 (#4423) (#4427) * chore: Adds 1.3.11 release notes * chore: Adds 1.3.11 release notes updated --------- (cherry picked from commit 63908e83196d6fa09a4230897c48ebfacebfbd25) Signed-off-by: Ashwin P Chandran Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] * Updates Release notes for 1.3.11 (#4428) (#4430) * chore: Adds 1.3.11 release notes * chore: Adds 1.3.11 release notes updated * chore: Adds 1.3.11 release notes adds skipped changelog PR --------- (cherry picked from commit 7de483fa3c02e2c526d85db1ebdbcd57f63ce192) Signed-off-by: Ashwin P Chandran Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] * [Stylelint] Add typing to Stylelint rules (#4392) * Add typing to Stylelint rules Signed-off-by: Matt Provost * Extract get color property parent into function Signed-off-by: Matt Provost * Optchain instead of unwrapping source file Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost Co-authored-by: Josh Romero * [Vis colors] Update legacy mapped colors in charts plugin to use ouiPaletteColorBlind() (#4398) Signed-off-by: Manasvini B Suryanarayana Co-authored-by: Ashwin P Chandran * [CVE-2022-25883] Resolve semver to 7.5.3 and remove unused package (#4411) In this PR, we resolve semver to 7.5.3 from 5.x, 6.x and 7.x. There are breaking changes in API in 7.5.3 compared to 5.x/6.x. However, these API changes do not impact any usages. Issue Resolve https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4370 Signed-off-by: ananzh Co-authored-by: Ashwin P Chandran * Enable sample data with Multiple datasource frontend (#4412) Signed-off-by: Kristen Tian * Feature (home): Add vis audit sample dashboard (#4339) * Feature (home): Add vis audit sample dashboard Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Add fonts for previewing the new theme (#4381) Also: * Expose default font-family values as CSS variables * Make default fonts load from OSD and not OUI * Make fonts differentiable across themes * Use the theme font in the Legacy Editor Signed-off-by: Miki Co-authored-by: Josh Romero * possible fix for flakey ci9 test (#4450) * possible fix for flakey ci9 test Signed-off-by: Ashwin P Chandran * fix syntax Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran * Chore (VisBuilder): Update icon to use OUI icon (#4446) * Chore (VisBuilder): Update icon to use OUI icon Fixes https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3691 Signed-off-by: Josh Romero * update changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Relocate tutorials imagery (#4382) Signed-off-by: Miki * Update main menu to display 'Dashboards' for consistency (#4453) * Update main menu to display 'Dashboards' for consistency. Fixes #4296 This resolves the inconsistency highlighted in issue #4296 (https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4296). The decision to make this change was made in issue #68 (https://github.com/opensearch-project/ux/issues/68)." * Update OpenSearch Dashboard to OpenSearch Dashboards for consistence * CHANGELOG.md update --------- Signed-off-by: Danila Gulderov * Adding Matt as a maintainer (#4469) * Chore: Add Matt as a maintainer * Adds changelog --------- Signed-off-by: Ashwin P Chandran * Add `color-scheme` to the root styling (#4477) Signed-off-by: Miki * Refactor hardcoded color to use OUI in `region_map` (#4299) * Refactor hardcoded color to use OUI Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Console] Convert lib/mappings to TypeScript (#4008) * Convert mappings.js to TypeScript Signed-off-by: Sirazh Gabdullin * Convert mappings.test.js to TypeScript Signed-off-by: Sirazh Gabdullin * Update CHANGELOG.md Signed-off-by: Sirazh Gabdullin * Add test for getTypes with multi-index mode Signed-off-by: Sirazh Gabdullin * type update Signed-off-by: Sirazh Gabdullin * update typing Signed-off-by: Sirazh Gabdullin * CHANGELOG fix Signed-off-by: Sirazh Gabdullin * Changelog update Signed-off-by: Sirazh Gabdullin * Update Changelog Signed-off-by: Sirazh Gabdullin --------- Signed-off-by: Sirazh Gabdullin Signed-off-by: Kawika Avilla Signed-off-by: Josh Romero Signed-off-by: Anan Zhuang Co-authored-by: Kawika Avilla Co-authored-by: Qingyang(Abby) Hu Co-authored-by: Ashwin P Chandran Co-authored-by: Anan Zhuang Co-authored-by: Josh Romero * Enable plugins to augment visualizations with additional data and context (#4361) Signed-off-by: Tyler Ohlsen * Update header logo selection logic and assets (#4383) Signed-off-by: Miki * [CI] Split build and verify into parallel jobs (#4467) * Also made linter and NOTICE validation run only on Linux Signed-off-by: Miki * New management overview page and rename stack management to dashboard management (#4287) Support navigation changes for administrative features. This change includes * Rename stack management to Dashboard management * Add new management overview page * Replace Stack Management to Management overview page on home app categories page * Make home plugin optional for managemnet overview Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4132 --------- Signed-off-by: Hailong Cui Co-authored-by: Josh Romero Co-authored-by: Ashwin P Chandran * Retain the original sample data interface (#4526) Signed-off-by: Kristen Tian * [Vis Augmenter Add UT for few fns (#4516) * Add UT for few fns Signed-off-by: Tyler Ohlsen * add changelog entry Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen * Bump tough-cookie from 4.0.0 to 4.1.3 (#4531) Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3. - [Release notes](https://github.com/salesforce/tough-cookie/releases) - [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md) - [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3) --- updated-dependencies: - dependency-name: tough-cookie dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ZilongX <99905560+ZilongX@users.noreply.github.com> * [Vis Augmenter] Update base vis height in view events flyout (#4535) * [Vis Augmenter] Update base vis height in view events flyout Signed-off-by: Tyler Ohlsen * Update CHANGELOG Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen * Dashboard De-Angularization (#4502) Removes Angular from the `plugins/dashboard` utilizing React. This includes refactoring to address changes in state management but will require fast follow to address none blocking issues raised which can be found here: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3365 Partially Resolves: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3365 --- * Replace angular modules with react components * Use React to start up the dashboard app, and use react routing to configure basic routing for dashboard plugin. * [Dashboard De-Angular] Render dashboard listing page (#4015) * https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4000 * Render the dashboard listing component with basic functionalities: * When there is no dashboard, render the empty dashboard page * When there are dashboards, show the dashboard listing table * When click on the dashboard, show the editor page * Delete the dashboards when selected * Can use search bar to filter dashboard * Basic top nav bar for dashboard (#4108) * Basic top nav bar for dashboard * This PR will add basic structure to render top nav bar, including a basic implementation for dashboard app state. * Render editor page with basic nav actions (#4213) * Added dashboard embeddable container to render the dashboard editor page. * Add visualization (#4257) * Add and save visualization to dashboard * Render empty screen (#4346) * Render empty screen with correct edit and view view when creating a new dashboard. * Fix routing (#4357) * Fix the edit action routing on the dashboard listing page; also fix routing when the route has no match. Add '_g' param to the URL on both dashboard listing page and dashboard editor page. * [Dashboard De-Angular] Enable time filter functionalities (#4364) * Fix time filter on dashboard * Save dashboard with time restore * Dashboard be able to save query and app filter * Enable functional test for dashboard * Fix comments and add ui bootstrap back * No index pattern routing (#4401) * Should redirect to stack management page if there is no index pattern detected. * Add embed mode and other URL param options (#4407) * UI should render based on URL param options * [Dashboard De-Angular] Add dashboard class for discard flow (#3563) * Add Dashboard class for state managing * isDirty working for cancel flow * [Dashboard De-Angular] Add breadcrumb with view/edit mode and unsaved flow (#4479) * set isDirty back to false when saving successfully * Breadcrumb working * change to dashboards in breadcrumb * [Dashboard De-Angular] Enable URL title param for initial filter on dashboard listing (#4480) * Fix dashboard listing functional test * Can filter dashboards using URL title param * Fix the functional tests * [Dashboard De-Angular] Fix dashboard save and back button functional test (#4491) * fix copy on save and functional test 5 * Fix back button navigation * Fix version migration for panels * Fix conversions between saved panel and container panel type * Fix redundant browser update by re-structure app state and global state sync logic in order for back button to work, also fix the corresponding functional test * migration version * Add initialization dirty flag and fix full mode filter bar * [Dashboard De-Angular] Fix filter and query related functional tests in functional test group 3 (#4495) * fix index pattern window * Fix time filter and query related functional test in group 3 * [Dashboard De-Angular] Fix remaining functional tests (#4496) * fix dashboard state function test in group 4 * fix expanding panel * fix dashboard listing delete (#4508) * [Dashboard De-Angular] Initial clean up and linter fix (#4511) * Clean up linter issues * Add changelog and other fixes * [Dashboard De-Angular] Cypress fix (#4521) * fix cypress * refactor scss files * delete old unit test for state management * Refactor app state and cleanup unused imports (#4504) * Clean up app state for Dashboards plugin. * Removes the dashboard container hook in place of a single dashboard app state container * Still recovers some follow-ups and clean up * Skips test for rendering of a legacy test. * Set dashboard container functions and fix license headers (#4540) * Set dashboard container after defining functions * renderEmpty was not being set prior to the current container was being dispatched. * fix up license headers for new files * add TODOs from PR Signed-off-by: abbyhu2000 Signed-off-by: Kawika Avilla Co-authored-by: Qingyang(Abby) Hu Co-authored-by: Kawika Avilla Co-authored-by: Miki * Add v2.9.0 release notes (#4550) Signed-off-by: Manasvini B Suryanarayana * Fix line to vega conversion bug (#4554) * Fix line to vega conversion bug Signed-off-by: Ashish Agrawal * Update CHANGELOG and release notes Signed-off-by: Ashish Agrawal * Address comments Signed-off-by: Ashish Agrawal * Add more details to comment Signed-off-by: Ashish Agrawal * Not a released changed, so no need to document Signed-off-by: Ashish Agrawal --------- Signed-off-by: Ashish Agrawal * Fix Node.js download link (#4556) https://mirrors.nodejs.org/ is no longer available which prevents the ability from release builds being made. Restoring back the original link. Follow-up should be considered about a caching mechanism and param to the CLI to utilize another link or path. Signed-off-by: Miki * [Vis Augmenter] Fix stats API visualization ID bug (#4565) * Fix vis augmenter stats api vis ID map Signed-off-by: Tyler Ohlsen * Update changelog Signed-off-by: Tyler Ohlsen * Update tests Signed-off-by: Tyler Ohlsen * Remove changelog entry Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen * [CCI] Add new or remove extra tags and styles in `saved_objects_management` plugin (#4069) * Add new or remove extra tags and styles (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Remove extra Fragment tags (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak --------- Signed-off-by: Sergey Myssak Signed-off-by: Josh Romero Co-authored-by: Andrey Myssak Co-authored-by: Josh Romero * Add documentation to vis_augmenter (#4527) * Add documentation to vis_augmenter Signed-off-by: Tyler Ohlsen * Add more Signed-off-by: Tyler Ohlsen * Add section about settings Signed-off-by: Tyler Ohlsen * Minor nits Signed-off-by: Tyler Ohlsen * More nits Signed-off-by: Tyler Ohlsen * Update changelog Signed-off-by: Tyler Ohlsen * Update CHANGELOG.md Signed-off-by: Josh Romero --------- Signed-off-by: Tyler Ohlsen Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Vis Augmenter] Fix bug of undefined tooltip when all plugin layers are empty (#4577) Signed-off-by: Tyler Ohlsen * [Dashboards] restructure folder to be more cohesive with the project (#4575) Fast follow to: https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4502 * Cleanup to just use `utils` * Move empty screen into embeddables folder with related features * Get rid of the export file in favor matching other plugins * Combine folders of components that are related, e.g., top_nav Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4574 Signed-off-by: Kawika Avilla * chore (home): Update visual consistency dashboard TSVB colors (#4501) To be consistent with https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4363 The TSVB visualization saved object includes the default color value, so #4363 changed it for all new TSVB visualizations, but any existing saved objects, like those in the visualization consistency dashboard, need to be updated manually. Signed-off-by: Josh Romero * [VisLib] Replace legend color palette with OUI color palette (#4365) * [VisLib] Replace legend color palette with OUI color palette Replace hard-coded palette (of 8 colors with 7 variations each) with rotations from euiPaletteColorBlind (of 10 colors with 7 variations each) Fixes https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4321 Signed-off-by: Josh Romero * update changelog Signed-off-by: Josh Romero * Update legend unit test Signed-off-by: Josh Romero * Update hard-coded legend color values in functional tests Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Bump word-wrap from 1.2.3 to 1.2.4 (#4589) * Bump word-wrap from 1.2.3 to 1.2.4 Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Add changelog Signed-off-by: Josh Romero --------- Signed-off-by: dependabot[bot] Signed-off-by: Josh Romero Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Josh Romero * Removed KUI usage in maps_legacy plugin (#3998) * Removed KUI usage from map_legacy plugin Signed-off-by: Malika Shamgunova * Removed KUI icon from maps_legacy plugin Signed-off-by: Malika Shamgunova * import Signed-off-by: Malika Shamgunova * Added to CHNAGELOG.md Signed-off-by: Malika Shamgunova * Updated CHANGELOG.md Signed-off-by: Malika Shamgunova * Update in CHANGELOG.md Signed-off-by: Malika Shamgunova * Upgrade the backport workflow (#4343) * Copy over the labels from the original PR * Label the backport PR with `autocut` * Label a PR that fails to backport Signed-off-by: Miki * Hide any output from `use_node` checking for Node compatibility (#4237) Signed-off-by: Miki Signed-off-by: Josh Romero Co-authored-by: Josh Romero * Resolved merge conflicts Signed-off-by: Malika Shamgunova * Apply suggestions from code review Resolve changelog conflicts Signed-off-by: Josh Romero --------- Signed-off-by: Malika Shamgunova Signed-off-by: Anan Zhuang Signed-off-by: Miki Signed-off-by: Josh Romero Co-authored-by: Qingyang(Abby) Hu Co-authored-by: Anan Zhuang Co-authored-by: Miki Co-authored-by: Josh Romero * Feature (home): Update visual consistency sample dashboard with more vis (#4581) * Feature (home): Update visual consistency sample dashboard with more vis Signed-off-by: Josh Romero * Use the correct playground URL Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Optimize `augment-vis` saved obj searching by adding arg to saved obj client (#4595) Signed-off-by: Tyler Ohlsen * [Markdown] Replace custom css styles and native html with OUI. (#4390) * replace custom styling Signed-off-by: Sirazh Gabdullin * update CHANGELOG Signed-off-by: Sirazh Gabdullin * Update CHANGELOG.md Co-authored-by: Miki Signed-off-by: Josh Romero --------- Signed-off-by: Sirazh Gabdullin Signed-off-by: Josh Romero Co-authored-by: Josh Romero Co-authored-by: Miki * Add resource ID filtering in fetch `augment-vis` obj queries (#4608) Signed-off-by: Tyler Ohlsen * Fix (styles): Make ace code editor themes consistent (#4609) * Fix (styles): Make ace code editor themes consistent Use "textmate" theme everywhere Update override selector to apply to root, to style portaled components, too (such as filter editor) Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Enable theme-switching via Advanced Settings to preview the Next theme (#4475) * Enable theme-switching via Advanced Settings to preview the Next theme Also: * Remove the unused "v8 (beta)" theme * Remove the overrides that locked in the light default theme * Correct theme version selection logic in the legacy UI renderer * Use the latest preview of OUI Signed-off-by: Miki * Enhance ComboBox handling in functional tests Signed-off-by: Miki --------- Signed-off-by: Miki * [Console] Converted all ```/lib/autocomplete/**/*.js``` files to typescript (#4148) Major changes are: * Convert autocomplete part to TS * reafactor and improve typing * clean comments for compileBodyDescription * refactor getTemplate --------- Signed-off-by: Sirazh Gabdullin Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Console] Convert all non-autocomplete lib files to typescript (#4150) * Convert non-autocomplete part to TS Signed-off-by: Sirazh Gabdullin * Update CHANGELOG.md Signed-off-by: Sirazh Gabdullin * refactor and improve typing Signed-off-by: Sirazh Gabdullin --------- Signed-off-by: Sirazh Gabdullin * [Table Visualization] Replace div containers with OuiFlex components (#4272) * replace div containers with OuiFlex Signed-off-by: Sirazh Gabdullin * Update test to not include removed class Signed-off-by: Sirazh Gabdullin * Update Changelog Signed-off-by: Sirazh Gabdullin * wrap table in FlexItem Signed-off-by: Sirazh Gabdullin --------- Signed-off-by: Sirazh Gabdullin Signed-off-by: Josh Romero Co-authored-by: Josh Romero * Chore(CHANGELOG): Update to CHANGELOG post 2.9 release (#4625) Signed-off-by: Manasvini B Suryanarayana * Refactor/remove breadcrumb styling main (#4621) * chore(chrome): Remove OSD breadcrumb styling and classes Now that the breacrumb styling is coming from OUI Signed-off-by: Josh Romero * chore (chrome): Remove other remnants of breadcrumb styling Essentially reverting: - https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1954 - https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2085 Signed-off-by: Josh Romero * Update snapshot Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero * Update header snapshot Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Feat (home): Add remaining vis type examples (#4619) * Feat (home): Add remaining vis type examples - Add and improve vega equivalents Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * [i18n] fix generation scripts (#4252) * [i18n] fix generation scripts Gave file permissions to the i18n scripts Generated: https://github.com/opensearch-project/dashboards-i18n/pull/25 With: ``` ./scripts/use_node scripts/i18n_extract.js --output-dir plugins/dashboards-i18n/translations/ ``` Had to fix some issues generating the scripts for example incorrect namespacing. Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/867 Signed-off-by: Kawika Avilla * update snapshot Signed-off-by: Kawika Avilla --------- Signed-off-by: Kawika Avilla Signed-off-by: Qingyang(Abby) Hu Co-authored-by: Ashwin P Chandran Co-authored-by: Qingyang(Abby) Hu * Feat (Discover): Update styles to be compatible with next theme (#4644) * Feat (Discover): Update styles to be compatible with next theme 1. Change doc table source highlight to use standard color functions instead of transparency 2. Build KUI CSS for next themes in OUI, and conditionally load to ensure surrounding doc view styled correctly 3. Update histogram styles to follow design guidance and avoid theme-specific imports Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Fix (Legacy Maps): Add necessary specificity for dark mode style over… (#4658) * Fix (Legacy Maps): Add necessary specificity for dark mode style overrides - wrap override styles in visualization selector - remove temp SASS var - update attribution background to be opaque for consistency with other controls - add raster tile filter to map tiles in dark mode, since OpenSearch doesn't serve dark mode raster tilesets - Fix tooltip behavior so that tooltip only appears when there's content to render fixes https://github.com/opensearch-project/dashboards-maps/issues/449, fixes https://github.com/opensearch-project/dashboards-maps/issues/450 Signed-off-by: Josh Romero * update changelog Signed-off-by: Josh Romero * Fix spacing linter issues Signed-off-by: Josh Romero * Update font-family overrides to use CSS var definitions Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Bump `node-sass` to a version that uses a newer `libsass` (#4651) Also: * bump `sass-loader` to a version that supports this `node-sass` Signed-off-by: Miki * Update webpack environment targets (#4649) Also: * Bump browserslist * Widen browser support matrix * Update browser typescript target to ES2018 * Bump `autoprefixer` but remove its usage as it spams the logs about it being unnecessary Signed-off-by: Miki * Reduce the amount of comments in compiled CSS (#4648) Signed-off-by: Miki * units test for utils folder (#4641) Signed-off-by: abbyhu2000 * Fix --font-text CSS var usage and add more leaflet font overrides (#4674) * Add missing quotes to --font-text CSS vars Signed-off-by: Josh Romero * Fix usage of --font-text CSS var Remove quotes Add additional leaflet font family overrides Move legacy map custom button styles to separate file Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Add saved objects service status api (#4696) Signed-off-by: Bandini Bhopi * Chore: Add 1.3.12 release notes (#4690) (#4709) * Chore: Add 1.3.12 release notes and update changelog Signed-off-by: Josh Romero * Add entries for late-resolving CVEs Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero (cherry picked from commit 492b055041dc02910b431c3d56fd98d20acac099) Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] * Test (linkchecker): Exclude checking dead link (#4720) Link in copyright header must be preserved, but we shouldn't check for it Signed-off-by: Josh Romero * [@osd/pm] Automate multi-target bootstrap and build (#4650) Also: * build @osd/std for multiple targets * convert @osd/i18n and @osd/ace from custom build scripts to targeted build process * have @osd/optimizer ignore already built artifacts Signed-off-by: Miki * [chore] Update CHANGELOG.md with 1.3.12 release (#4739) Signed-off-by: Manasvini B Suryanarayana * Feat (home): Remove color customizations from sample dashboards (#4741) * Feat (home): Remove color customizations from sample dashboards So that they better reflect default color palettes and theming - Remove `uiStateJSON` `vis.colors` and `vis.defaultColors` customizations - Remove `embeddableConfig` `vis.colors` and `vis.defaultColors` cusomtizations from dashboard `panelsJSON` - TSVB visualizations require explicit color picker values - use only defined categorical palette values - Replace named and explicit colors in vega spec with OUI palette values - Add missing `time_field` and `index_pattern` properties to TSVB configs that were missing them - Update vega/vega-lite default config to use `OuiTextColor` for `text` marks Signed-off-by: Josh Romero * update changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * Bump version of tinygradient from 0.4.3 to 1.1.5 (#4742) * Bump version of tinygradient from 0.4.3 to 1.1.5 Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Update to use carat version Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost * Remove visualization editor background (#4719) * Remove vis editor background Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Dashboard De-Angular] Add unit tests for dashboard_listing and dashboard_top_nav (#4640) * Add unit tests Signed-off-by: abbyhu2000 * fix other unit tests and update snapshots Signed-off-by: abbyhu2000 * update snapshots Signed-off-by: abbyhu2000 * fix osdUrlStateStorage Signed-off-by: abbyhu2000 * fix snapshots Signed-off-by: abbyhu2000 --------- Signed-off-by: abbyhu2000 * [Stylelint] Add no_restricted_values linter rule (#4413) * Add no_restricted_values linter rule Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Add explanation for file based configs Signed-off-by: Matt Provost * Update error messages Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost Signed-off-by: Matt Provost Co-authored-by: Ashwin P Chandran Co-authored-by: Anan Zhuang Co-authored-by: Josh Romero * Add @curq as co-maintainer (#4760) * Add @curq as co-maintainer Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * [Tests] Add BWC tests for 2.9 and 2.10 versions (#4762) Signed-off-by: Manasvini B Suryanarayana * Updated README.md (#4769) Changed a Typo in README.md file Signed-off-by: Suyash Srivastava * Refactor logo usage (#4702) Also: * Move logos to a central location * Make the loading spinner color-scheme-aware * Recreate `OverviewPageHeader`, `HomeIcon`, `HeaderLogo`, `SolutionTitle`, `Welcome`, `Overview` tests * Enhance `ExitFullScreenButton`, `Header` tests * Tinified favicon assets Signed-off-by: Miki * fixes merge conflict resolution error Signed-off-by: Ashwin P Chandran * fixes snapshot Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Bandini Bhopi Signed-off-by: David Sinclair Signed-off-by: David Sinclair Signed-off-by: Ashish Agrawal Signed-off-by: Alexei Karikov Signed-off-by: Su Signed-off-by: Aigerim Suleimenova Signed-off-by: Josh Romero Signed-off-by: Kristen Tian Signed-off-by: Miki Signed-off-by: Manasvini B Suryanarayana Signed-off-by: Kawika Avilla Signed-off-by: Matt Provost Signed-off-by: Sergey Myssak Signed-off-by: Andrey Myssak Signed-off-by: Ashwin P Chandran Signed-off-by: github-actions[bot] Signed-off-by: ananzh Signed-off-by: Danila Gulderov Signed-off-by: Sirazh Gabdullin Signed-off-by: Anan Zhuang Signed-off-by: Tyler Ohlsen Signed-off-by: Hailong Cui Signed-off-by: dependabot[bot] Signed-off-by: abbyhu2000 Signed-off-by: Ashish Agrawal Signed-off-by: Malika Shamgunova Signed-off-by: Qingyang(Abby) Hu Signed-off-by: Matt Provost Signed-off-by: Suyash Srivastava Co-authored-by: Bandini <63824432+bandinib-amzn@users.noreply.github.com> Co-authored-by: David Sinclair <24573542+sikhote@users.noreply.github.com> Co-authored-by: Ashish Agrawal Co-authored-by: Alexei Karikov Co-authored-by: Zhongnan Su Co-authored-by: Aigerim Suleimenova Co-authored-by: Josh Romero Co-authored-by: Kristen Tian <105667444+kristenTian@users.noreply.github.com> Co-authored-by: Miki Co-authored-by: Manasvini B Suryanarayana Co-authored-by: Kawika Avilla Co-authored-by: Matt Provost Co-authored-by: Anan Zhuang Co-authored-by: Sergey Myssak Co-authored-by: Andrey Myssak Co-authored-by: Andrey Myssak <40265277+andreymyssak@users.noreply.github.com> Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: gulderov Co-authored-by: Sirazh Gabdullin Co-authored-by: Qingyang(Abby) Hu Co-authored-by: Tyler Ohlsen Co-authored-by: Hailong Cui Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ZilongX <99905560+ZilongX@users.noreply.github.com> Co-authored-by: Miki Co-authored-by: miamia1999 <123776561+miamia1999@users.noreply.github.com> Co-authored-by: Suyash Srivastava commit 8c2e8bf659bf7a3a8fad88c62f48ce350c4c5ce5 Author: Ashwin P Chandran Date: Tue Aug 22 19:29:16 2023 -0700 [Data Explorer] Altrnate result state views + fixes (#4764) * multiple minor fixes and no results view Signed-off-by: Ashwin P Chandran * Apply suggestions from code review Co-authored-by: Miki Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran Signed-off-by: Ashwin P Chandran Co-authored-by: Miki commit c1ba8cd2f8de4082bc0849a3b57d7634d47c5551 Author: Qingyang(Abby) Hu Date: Mon Aug 21 16:23:13 2023 -0700 Merge main to feature (#4789) * [Saved Object Service] Adds Repository Factory Provider (#4149) * Adds Repository Factory Provider Signed-off-by: Bandini Bhopi * add category option for context menus (#4144) * enhance grouping for context menu options Signed-off-by: David Sinclair * change log Signed-off-by: David Sinclair * remove type export Signed-off-by: David Sinclair * revert border and prevent destroy options Signed-off-by: David Sinclair * update comments for building panels Signed-off-by: David Sinclair * build panels tests and more comments Signed-off-by: David Sinclair * add category option for context menus Signed-off-by: David Sinclair * changelog Signed-off-by: David Sinclair * add order to groups Signed-off-by: David Sinclair * documentation, shorter copyrighty, minor cleanup Signed-off-by: David Sinclair * changelog Signed-off-by: David Sinclair --------- Signed-off-by: David Sinclair Signed-off-by: David Sinclair Signed-off-by: Ashish Agrawal Co-authored-by: Ashish Agrawal * [CCI] Add bluebird replaces for src/plugins/saved_objects (#4026) * Add bluebird replaces for src/plugins/saved_objects * Add changelog entry --------- Signed-off-by: Alexei Karikov * Validate and correct change log after 2.8 release (#4275) Signed-off-by: Su * [DEVELOPER_GUIDE] resolving links (#3989) * links Signed-off-by: Aigerim Suleimenova * new section for doveloper guide Signed-off-by: Aigerim Suleimenova * updates Signed-off-by: Aigerim Suleimenova * Update DEVELOPER_GUIDE.md Co-authored-by: Ashwin P Chandran Signed-off-by: Aigerim Suleimenova * Update DEVELOPER_GUIDE.md Signed-off-by: Josh Romero --------- Signed-off-by: Aigerim Suleimenova Signed-off-by: Josh Romero Co-authored-by: Ashwin P Chandran Co-authored-by: Josh Romero * Enable data client with sample data server side (#4268) * Enable data client with sample data server side * Add dataSourceId into savedObject Signed-off-by: Kristen Tian * Functional list, install uninstall Signed-off-by: Kristen Tian * add change log Signed-off-by: Kristen Tian * address comments Signed-off-by: Kristen Tian * add ut Signed-off-by: Kristen Tian --------- Signed-off-by: Kristen Tian * Upgrade the backport workflow (#4343) * Copy over the labels from the original PR * Label the backport PR with `autocut` * Label a PR that fails to backport Signed-off-by: Miki * Hide any output from `use_node` checking for Node compatibility (#4237) Signed-off-by: Miki Signed-off-by: Josh Romero Co-authored-by: Josh Romero * [Vis Colors] Update default color in TSVB to use `ouiPaletteColorBlind()[0]` (#4363) Signed-off-by: Manasvini B Suryanarayana * Add BWC tests for 2.7 and 2.8 (#4023) Signed-off-by: Manasvini B Suryanarayana * [Vis colors] Replace vis_type_timeline colors with `ouiPaletteColorBlind()` (#4366) Signed-off-by: Manasvini B Suryanarayana * [Lint] add custom stylelint rules and config (#4290) * [Lint] add custom stylelint rules and config Adding `@osd/stylelint-config` and `@osd/stylelint-plugin-stylelint` packages. These packages are utilized by OSD core and can be ran with the following: `yarn lint:style` Can be used to fix known non-compliant styling with the following: `yarn lint:style --fix` Can be used to audit untracked styling (based on defined rules) with the following: ``` export OUI_AUDIT_ENABLED=true yarn lint:style ``` --- `@osd/stylelint-config` Defines rules approved by UX and OSD core in JSON files and is added to OSD core. Within this commit is defined `colors.json` and `global_selectors.json`. `colors.json` defines a property that can be matched with a regex of a selector. If the selector is tracked it will have an `approved` value and a list of `rejected` values that UX knows if a value should be something. `global_selectors.json` defines a selector that if tracked, it will have an `approved` list of relative paths to files that can modify the global selector. --- `@osd/stylelint-plugin-stylelint` Creates the functionality that utilizes the JSON files within the `@osd/stylelint-config`. Within this commit is defined `no_custom_colors` and `no_modifying_global_selectors` rules. `no_custom_colors` checks if a property is a color property. It then utilizes a compliance engine helper to check the `colors.json` to see if the property being modified has a compliance rule available for the property for the specific selector and if it is not compliant. For example, if a selector matches `button` and we are trying to apply `background-color: red` to it. Stylelint will catch this and flag this as a known non-compliance issue since it knows that it should `$euiColorWarning`. If we pass `--fix` the property will be updated to be `$euiColorWarning`. If `OUI_AUDIT_ENABLED` is true it will catch all `background-color` being modified that is not being defined explicitly in `colors.json` `no_modifying_global_selectors` checks if a selector being modified is defined in `global_selectors.json` to see if a selector not defined in a specific list of approved files. For example, if a selector matches `#opensearch-dashboards-body` and it is being modified in `src/core/public/rendering/_base.scss`. Stylelint will catch this and flag this as a non-compliance issue. Since no other file should be modifying this selector. If we pass `--fix` the styling will be complete removed from the non-compliant file. --- Next steps: * Migrate these packages to OUI * Consider adding `yarn lint:style --fix` to the build release script here: https://github.com/opensearch-project/opensearch-build/blob/main/scripts/default/opensearch-dashboards/build.sh#L89 Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4246 Signed-off-by: Kawika Avilla * fix to use find Signed-off-by: Kawika Avilla * Add regex matching and OUI modification lint Signed-off-by: Matt Provost * add changelog Signed-off-by: Josh Romero * address issues Signed-off-by: Kawika Avilla --------- Signed-off-by: Kawika Avilla Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Matt Provost Co-authored-by: Josh Romero * Fix linked deps resolution (#4342) Signed-off-by: Miki * Add configurable `defaults` to `uiSettings` (#4344) Also now: * `theme:darkMode` and `theme:version` can be configured via `defaults` * unauthenticated users are no longer forced to light mode Signed-off-by: Miki * Refactor hardcode color to use OUI in `maps_legacy` (#4294) * Refactor color to use OUI * Pull theme value from actual active theme * Update changelog --------- Signed-off-by: Matt Provost Signed-off-by: Josh Romero Co-authored-by: Anan Zhuang Co-authored-by: Josh Romero * Remove broken flot documentation link for Ruby API (#4384) * Remove broken documentation link for Ruby API https://apidock.com/ruby/Time/to_i is currently down for maintenance But we don't need this link anyway, because it's talking about standard methods. And we plan to deprecate flot_charts altogether: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4267 Signed-off-by: Josh Romero * remove leftover link brackets Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero * [CCI] Fix relationships header overflow (#4070) * Fix relationships header overflow (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Replace relationships css file with oui classname (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Make title overflow wrap instead of truncation (#3967) Co-authored-by: Andrey Myssak Signed-off-by: Sergey Myssak * Remove icon from flyout header and wrap title Signed-off-by: Josh Romero --------- Signed-off-by: Sergey Myssak Signed-off-by: Josh Romero Co-authored-by: Andrey Myssak Co-authored-by: Josh Romero * Refactor color maps to use OUI color palettes (#4293) * Remove color_util Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost * Revert "Remove color_util" This reverts commit 9ca9c56e6bc5d2750971e04a2df7028f5c472b8b. Signed-off-by: Matt Provost * Refactor color maps to use Oui color palettes Signed-off-by: Matt Provost * Update changelog pt 2: electric boogaloo Signed-off-by: Matt Provost * Make gradients look better Signed-off-by:… * adds Changelog Signed-off-by: Ashwin P Chandran * minor fixes before merge to main Signed-off-by: Ashwin P Chandran * PR comment fixes Signed-off-by: Ashwin P Chandran * fixes reset search Signed-off-by: Ashwin P Chandran * fix save query management Signed-off-by: Ashwin P Chandran * [Data Explorer][Discover 2.0] Enable sort and cell actions for table (#4787) * [Data Explorer][Discover 2.0] Enable sort and cell actions for table Signed-off-by: ananzh * update test snapshot Signed-off-by: ananzh --------- Signed-off-by: ananzh (cherry picked from commit 87663957200bb1e06253dc4fbe7e471a268fd142) * [Data Explorer][Discover 2.0] Change to inspect icon and update hint msg (#4827) Issue Resolve https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4826 Signed-off-by: ananzh (cherry picked from commit bef76fee6074c7bd9ba45e2f0afe6716af38768d) * updates snapshot Signed-off-by: Ashwin P Chandran * Allows for more than 2 level path be shared Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran Co-authored-by: Anan Zhuang (cherry picked from commit 4f909f9aafb729248d8ccc71c52096d5c15b223a) Signed-off-by: Ashwin P Chandran --- .../config/global_selectors.json | 6 +- .../config/restricted_properties.json | 7 +- src/plugins/data_explorer/.i18nrc.json | 7 + src/plugins/data_explorer/README.md | 11 + src/plugins/data_explorer/common/index.ts | 7 + .../data_explorer/opensearch_dashboards.json | 18 + .../data_explorer/public/application.tsx | 41 ++ .../__snapshots__/app_container.test.tsx.snap | 13 + .../data_explorer/public/components/app.tsx | 36 ++ .../public/components/app_container.scss | 18 + .../public/components/app_container.test.tsx | 45 ++ .../public/components/app_container.tsx | 39 ++ .../public/components/no_view.tsx | 40 ++ .../public/components/sidebar/index.tsx | 120 +++++ src/plugins/data_explorer/public/index.scss | 9 + src/plugins/data_explorer/public/index.ts | 17 + src/plugins/data_explorer/public/plugin.ts | 140 +++++ .../public/services/view_service/index.ts | 7 + .../public/services/view_service/types.ts | 43 ++ .../public/services/view_service/view.ts | 31 ++ .../view_service/view_service.test.ts | 63 +++ .../services/view_service/view_service.ts | 88 ++++ src/plugins/data_explorer/public/types.ts | 37 ++ .../data_explorer/public/utils/mocks.ts | 35 ++ .../public/utils/state_management/hooks.ts | 14 + .../public/utils/state_management/index.ts | 7 + .../utils/state_management/metadata_slice.ts | 56 ++ .../public/utils/state_management/preload.ts | 47 ++ .../redux_persistence.test.tsx | 45 ++ .../state_management/redux_persistence.ts | 30 ++ .../public/utils/state_management/store.ts | 89 ++++ .../data_explorer/public/utils/use/index.ts | 6 + .../public/utils/use/use_view.ts | 35 ++ src/plugins/data_explorer/server/index.ts | 16 + src/plugins/data_explorer/server/plugin.ts | 41 ++ .../data_explorer/server/routes/index.ts | 22 + src/plugins/data_explorer/server/types.ts | 9 + src/plugins/discover/common/index.ts | 29 +- .../discover/opensearch_dashboards.json | 3 +- .../public/__mock__/index_pattern_mock.ts | 100 ++++ .../application/components/chart/chart.tsx | 102 ++++ .../components/chart/histogram/histogram.tsx | 360 +++++++++++++ .../components/chart/histogram/type.ts | 16 + .../hits_counter/hits_counter.test.tsx | 0 .../chart/hits_counter/hits_counter.tsx | 95 ++++ .../{ => chart}/hits_counter/index.ts | 0 .../chart/timechart_header/index.ts | 31 ++ .../timechart_header.test.tsx | 0 .../timechart_header/timechart_header.tsx | 189 +++++++ .../chart/utils/create_histogram_configs.ts | 29 ++ .../chart/utils/get_dimension.test.ts | 74 +++ .../components/chart/utils/get_dimensions.ts | 73 +++ .../components/chart/utils/index.ts | 8 + .../chart/utils/point_series.test.ts | 62 +++ .../components/chart/utils/point_series.ts | 122 +++++ .../components/data_grid/constants.ts | 12 + .../components/data_grid/data_grid_table.tsx | 165 ++++++ .../data_grid_table_cell_actions.tsx | 76 +++ .../data_grid_table_cell_value.test.tsx | 187 +++++++ .../data_grid/data_grid_table_cell_value.tsx | 80 +++ .../data_grid_table_columns.test.tsx | 249 +++++++++ .../data_grid/data_grid_table_columns.tsx | 69 +++ .../data_grid/data_grid_table_context.tsx | 23 + ...data_grid_table_docview_inspect_button.tsx | 28 + .../data_grid/data_grid_table_flyout.tsx | 77 +++ .../doc_viewer/doc_viewer_render_tab.tsx | 1 - .../doc_viewer_links.test.tsx.snap | 4 + .../doc_viewer_links/doc_viewer_links.tsx | 4 +- .../components/help_menu/help_menu_util.ts | 49 ++ .../loading_spinner/loading_spinner.scss | 4 + .../loading_spinner/loading_spinner.tsx | 24 +- .../components/no_results/no_results.tsx | 212 ++++++++ .../sidebar/discover_field.test.tsx | 44 +- .../components/sidebar/discover_field.tsx | 140 ++--- .../sidebar/discover_field_details.tsx | 1 - .../sidebar/discover_field_search.test.tsx | 16 +- .../sidebar/discover_field_search.tsx | 76 ++- .../components/sidebar/discover_sidebar.scss | 100 +--- .../sidebar/discover_sidebar.test.tsx | 64 ++- .../components/sidebar/discover_sidebar.tsx | 368 +++++++------ .../components/sidebar/lib/group_fields.tsx | 7 + .../skip_bottom_button/skip_bottom_button.tsx | 2 +- .../application/components/table/table.scss | 3 + .../application/components/table/table.tsx | 5 +- .../components/table/table_helper.tsx | 7 - ...s.snap => open_search_panel.test.tsx.snap} | 0 .../components/top_nav/get_top_nav_links.tsx | 339 ++++++++++++ .../top_nav/open_search_panel.test.tsx | 53 ++ .../components/top_nav/open_search_panel.tsx | 127 +++++ .../top_nav/show_open_search_panel.tsx | 70 +++ .../uninitialized}/uninitialized.tsx | 0 .../components/utils/use_pagination.test.ts | 73 +++ .../components/utils/use_pagination.ts | 39 ++ .../doc_views/doc_views_registry.ts | 16 - .../application/doc_views/doc_views_types.ts | 9 - .../public/application/utils/columns.test.ts | 24 + .../public/application/utils/columns.ts | 43 ++ .../utils/state_management/common.test.ts | 33 ++ .../utils/state_management/common.ts | 28 + .../state_management/discover_slice.test.tsx | 85 +++ .../utils/state_management/discover_slice.tsx | 166 ++++++ .../utils/state_management/index.ts | 17 + .../canvas/discover_chart_container.scss | 25 + .../canvas/discover_chart_container.tsx | 42 ++ .../view_components/canvas/discover_table.tsx | 98 ++++ .../view_components/canvas/index.tsx | 82 +++ .../view_components/canvas/top_nav.tsx | 80 +++ .../view_components/context/index.tsx | 42 ++ .../application/view_components/index.ts | 7 + .../view_components/panel/index.tsx | 67 +++ .../view_components/utils/get_default_sort.ts | 50 ++ .../view_components/utils/get_sort.test.ts | 107 ++++ .../view_components/utils/get_sort.ts | 92 ++++ .../utils/get_sort_for_search_source.ts | 66 +++ .../utils/index_pattern_helper.ts | 114 ++++ .../utils/update_search_source.ts | 72 +++ .../utils/use_index_pattern.ts | 51 ++ .../view_components/utils/use_search.ts | 287 +++++++++++ src/plugins/discover/public/build_services.ts | 13 +- src/plugins/discover/public/index.ts | 4 +- src/plugins/discover/public/migrate_state.ts | 138 +++++ .../public/opensearch_dashboards_services.ts | 21 +- src/plugins/discover/public/plugin.ts | 272 ++++------ .../public/saved_searches/_saved_search.ts | 2 + .../discover/public/saved_searches/types.ts | 12 +- src/plugins/discover/server/ui_settings.ts | 12 + src/plugins/discover_legacy/README.md | 1 + src/plugins/discover_legacy/common/index.ts | 41 ++ .../opensearch_dashboards.json | 27 + .../public/application/_discover.scss | 0 .../public/application/angular/_index.scss | 0 .../public/application/angular/context.html | 0 .../public/application/angular/context.js | 0 .../application/angular/context/NOTES.md | 0 .../application/angular/context/_index.scss | 0 .../application/angular/context/api/_stubs.js | 0 .../application/angular/context/api/anchor.js | 0 .../angular/context/api/anchor.test.js | 0 .../context/api/context.predecessors.test.js | 0 .../context/api/context.successors.test.js | 0 .../angular/context/api/context.ts | 0 .../context/api/utils/date_conversion.test.ts | 0 .../context/api/utils/date_conversion.ts | 0 .../api/utils/fetch_hits_in_interval.ts | 0 .../context/api/utils/generate_intervals.ts | 0 .../get_opensearch_query_search_after.ts | 0 .../api/utils/get_opensearch_query_sort.ts | 0 .../angular/context/api/utils/sorting.test.ts | 0 .../angular/context/api/utils/sorting.ts | 0 .../components/action_bar/_action_bar.scss | 0 .../context/components/action_bar/_index.scss | 0 .../components/action_bar/action_bar.test.tsx | 0 .../components/action_bar/action_bar.tsx | 0 .../action_bar/action_bar_directive.ts | 0 .../action_bar/action_bar_warning.tsx | 0 .../context/components/action_bar/index.ts | 0 .../helpers/call_after_bindings_workaround.js | 0 .../angular/context/query/actions.js | 0 .../angular/context/query/constants.js | 0 .../angular/context/query/index.js | 0 .../angular/context/query/state.js | 0 .../context/query_parameters/actions.js | 0 .../context/query_parameters/actions.test.ts | 0 .../context/query_parameters/constants.ts | 0 .../angular/context/query_parameters/index.js | 0 .../angular/context/query_parameters/state.ts | 0 .../application/angular/context_app.html | 0 .../public/application/angular/context_app.js | 0 .../application/angular/context_state.test.ts | 0 .../application/angular/context_state.ts | 0 .../__snapshots__/no_results.test.js.snap | 0 .../angular/directives/_histogram.scss | 0 .../angular/directives/_index.scss | 0 .../angular/directives/_no_results.scss | 0 .../angular/directives/debounce/debounce.js | 0 .../directives/debounce/debounce.test.ts | 0 .../angular/directives/debounce/index.js | 0 .../angular/directives/fixed_scroll.js | 0 .../angular/directives/fixed_scroll.test.js | 0 .../angular/directives/histogram.tsx | 0 .../application/angular/directives/index.js | 0 .../angular/directives/no_results.js | 0 .../angular/directives/no_results.test.js | 0 .../angular/directives/render_complete.ts | 0 .../angular/directives/uninitialized.tsx | 78 +++ .../public/application/angular/discover.js | 18 + .../application/angular/discover_legacy.html | 0 .../angular/discover_state.test.ts | 0 .../application/angular/discover_state.ts | 0 .../public/application/angular/doc.html | 0 .../public/application/angular/doc.ts | 0 .../angular/doc_table/_doc_table.scss | 0 .../angular/doc_table/actions/columns.ts | 0 .../angular/doc_table/components/_index.scss | 0 .../doc_table/components/_table_header.scss | 0 .../tool_bar_pager_buttons.test.tsx.snap | 0 .../tool_bar_pager_text.test.tsx.snap | 0 .../doc_table/components/pager/index.ts | 0 .../pager/tool_bar_pager_buttons.test.tsx | 0 .../pager/tool_bar_pager_buttons.tsx | 0 .../pager/tool_bar_pager_text.test.tsx | 0 .../components/pager/tool_bar_pager_text.tsx | 0 .../doc_table/components/row_headers.test.js | 0 .../doc_table/components/table_header.ts | 0 .../__snapshots__/table_header.test.tsx.snap | 0 .../components/table_header/helpers.tsx | 0 .../table_header/table_header.test.tsx | 0 .../components/table_header/table_header.tsx | 0 .../table_header/table_header_column.tsx | 0 .../angular/doc_table/components/table_row.ts | 0 .../doc_table/components/table_row/_cell.scss | 0 .../components/table_row/_details.scss | 0 .../components/table_row/_index.scss | 0 .../doc_table/components/table_row/_open.scss | 0 .../doc_table/components/table_row/cell.html | 0 .../components/table_row/details.html | 0 .../doc_table/components/table_row/open.html | 0 .../table_row/truncate_by_height.html | 0 .../doc_table/create_doc_table_react.tsx | 0 .../angular/doc_table/doc_table.html | 0 .../angular/doc_table/doc_table.test.js | 0 .../angular/doc_table/doc_table.ts | 0 .../angular/doc_table/doc_table_strings.js | 0 .../application/angular/doc_table/index.scss | 0 .../application/angular/doc_table/index.ts | 0 .../angular/doc_table/infinite_scroll.ts | 0 .../angular/doc_table/lib/get_default_sort.ts | 0 .../angular/doc_table/lib/get_sort.test.ts | 0 .../angular/doc_table/lib/get_sort.ts | 0 .../lib/get_sort_for_search_source.ts | 0 .../angular/doc_table/lib/pager/index.js | 0 .../angular/doc_table/lib/pager/pager.js | 0 .../doc_table/lib/pager/pager_factory.ts | 0 .../public/application/angular/doc_viewer.tsx | 0 .../application/angular/doc_viewer_links.tsx | 0 .../application/angular/helpers/index.ts | 0 .../angular/helpers/point_series.ts | 0 .../public/application/angular/index.ts | 0 .../public/application/angular/redirect.ts | 0 .../application/angular/response_handler.js | 0 .../public/application/application.ts | 0 .../context_error_message.test.tsx | 0 .../context_error_message.tsx | 0 .../context_error_message_directive.ts | 0 .../components/context_error_message/index.ts | 0 .../create_discover_legacy_directive.ts | 0 .../components/discover_legacy.tsx | 32 +- .../application/components/doc/doc.test.tsx | 150 ++++++ .../public/application/components/doc/doc.tsx | 140 +++++ .../doc/use_opensearch_doc_search.test.tsx | 98 ++++ .../doc/use_opensearch_doc_search.ts | 114 ++++ .../__snapshots__/doc_viewer.test.tsx.snap | 56 ++ .../doc_viewer_render_tab.test.tsx.snap | 20 + .../components/doc_viewer/doc_viewer.scss | 72 +++ .../components/doc_viewer/doc_viewer.test.tsx | 94 ++++ .../components/doc_viewer/doc_viewer.tsx | 75 +++ .../doc_viewer/doc_viewer_render_error.tsx | 48 ++ .../doc_viewer/doc_viewer_render_tab.test.tsx | 52 ++ .../doc_viewer/doc_viewer_render_tab.tsx | 52 ++ .../components/doc_viewer/doc_viewer_tab.tsx | 101 ++++ .../doc_viewer_links.test.tsx.snap | 34 ++ .../doc_viewer_links.test.tsx | 68 +++ .../doc_viewer_links/doc_viewer_links.tsx | 35 ++ .../__snapshots__/field_name.test.tsx.snap | 94 ++++ .../components/field_name/field_name.test.tsx | 52 ++ .../components/field_name/field_name.tsx | 75 +++ .../components/field_name/field_type_name.ts | 85 +++ .../components/help_menu/help_menu_util.js | 0 .../hits_counter/hits_counter.test.tsx | 80 +++ .../components/hits_counter/hits_counter.tsx | 0 .../components/hits_counter/index.ts | 31 ++ .../json_code_block.test.tsx.snap | 20 + .../json_code_block/json_code_block.test.tsx | 46 ++ .../json_code_block/json_code_block.tsx | 45 ++ .../loading_spinner/loading_spinner.test.tsx | 45 ++ .../loading_spinner/loading_spinner.tsx | 47 ++ .../discover_index_pattern.test.tsx.snap | 0 .../sidebar/change_indexpattern.tsx | 0 .../components/sidebar/discover_field.scss | 4 + .../sidebar/discover_field.test.tsx | 152 ++++++ .../components/sidebar/discover_field.tsx | 245 +++++++++ .../sidebar/discover_field_bucket.scss | 4 + .../sidebar/discover_field_bucket.tsx | 133 +++++ .../sidebar/discover_field_details.scss | 0 .../sidebar/discover_field_details.test.tsx | 312 +++++++++++ .../sidebar/discover_field_details.tsx | 153 ++++++ .../sidebar/discover_field_search.test.tsx | 160 ++++++ .../sidebar/discover_field_search.tsx | 313 +++++++++++ .../sidebar/discover_index_pattern.test.tsx | 0 .../sidebar/discover_index_pattern.tsx | 0 .../sidebar/discover_index_pattern_title.tsx | 0 .../components/sidebar/discover_sidebar.scss | 99 ++++ .../sidebar/discover_sidebar.test.tsx | 148 ++++++ .../components/sidebar/discover_sidebar.tsx | 326 ++++++++++++ .../application/components/sidebar/index.ts | 31 ++ .../sidebar/lib/field_calculator.test.ts | 268 ++++++++++ .../sidebar/lib/field_calculator.ts | 148 ++++++ .../sidebar/lib/field_filter.test.ts | 107 ++++ .../components/sidebar/lib/field_filter.ts | 89 ++++ .../components/sidebar/lib/get_details.ts | 71 +++ .../sidebar/lib/get_field_type_name.ts | 85 +++ .../lib/get_index_pattern_field_list.ts | 53 ++ .../components/sidebar/lib/get_warnings.ts | 55 ++ .../sidebar/lib/group_fields.test.ts | 125 +++++ .../components/sidebar/lib/group_fields.tsx | 87 ++++ .../sidebar/lib/visualize_trigger_utils.ts | 122 +++++ .../sidebar/string_progress_bar.tsx | 46 ++ .../application/components/sidebar/types.ts | 52 ++ .../components/skip_bottom_button/index.ts | 31 ++ .../skip_bottom_button.test.tsx | 51 ++ .../skip_bottom_button/skip_bottom_button.tsx | 66 +++ .../components/table/table.test.tsx | 279 ++++++++++ .../application/components/table/table.tsx | 149 ++++++ .../components/table/table_helper.test.ts | 58 +++ .../components/table/table_helper.tsx | 43 ++ .../components/table/table_row.tsx | 129 +++++ .../table/table_row_btn_collapse.tsx | 56 ++ .../table/table_row_btn_filter_add.tsx | 69 +++ .../table/table_row_btn_filter_exists.tsx | 81 +++ .../table/table_row_btn_filter_remove.tsx | 69 +++ .../table/table_row_btn_toggle_column.tsx | 79 +++ .../table/table_row_icon_no_mapping.tsx | 59 +++ .../table/table_row_icon_underscore.tsx | 63 +++ .../components/timechart_header/index.ts | 0 .../timechart_header.test.tsx | 110 ++++ .../timechart_header/timechart_header.tsx | 0 .../open_search_panel.test.js.snap | 69 +++ .../components/top_nav/open_search_panel.js | 0 .../top_nav/open_search_panel.test.js | 0 .../top_nav/show_open_search_panel.js | 0 .../doc_views/doc_views_helpers.tsx | 0 .../doc_views/doc_views_registry.ts | 70 +++ .../application/doc_views/doc_views_types.ts | 86 ++++ .../doc_views_links_registry.ts | 18 + .../doc_views_links/doc_views_links_types.ts | 25 + .../application/embeddable/constants.ts | 0 .../public/application/embeddable/index.ts | 0 .../embeddable/search_embeddable.scss | 0 .../embeddable/search_embeddable.ts | 0 .../embeddable/search_embeddable_factory.ts | 0 .../embeddable/search_template.html | 0 .../public/application/embeddable/types.ts | 0 .../public/application/helpers/breadcrumbs.ts | 51 ++ .../helpers/format_number_with_commas.ts | 38 ++ .../helpers/get_index_pattern_id.ts | 71 +++ ...get_switch_index_pattern_app_state.test.ts | 0 .../get_switch_index_pattern_app_state.ts | 0 .../public/application/helpers/index.ts | 32 ++ .../helpers/migrate_legacy_query.ts | 48 ++ .../helpers/popularize_field.test.ts | 93 ++++ .../application/helpers/popularize_field.ts | 52 ++ .../helpers/shorten_dotted_string.ts | 37 ++ .../helpers/validate_time_range.test.ts | 58 +++ .../helpers/validate_time_range.ts | 61 +++ .../public/application/index.scss | 0 .../discover_legacy/public/build_services.ts | 129 +++++ .../public/get_inner_angular.ts | 0 src/plugins/discover_legacy/public/index.ts | 41 ++ src/plugins/discover_legacy/public/mocks.ts | 61 +++ .../public/opensearch_dashboards_services.ts | 129 +++++ src/plugins/discover_legacy/public/plugin.ts | 487 ++++++++++++++++++ .../public/saved_searches/_saved_search.ts | 86 ++++ .../public/saved_searches/index.ts | 32 ++ .../public/saved_searches/saved_searches.ts | 50 ++ .../public/saved_searches/types.ts | 47 ++ .../public/url_generator.test.ts | 269 ++++++++++ .../discover_legacy/public/url_generator.ts | 127 +++++ .../public/top_nav_menu/top_nav_menu_data.tsx | 2 + .../public/top_nav_menu/top_nav_menu_item.tsx | 39 +- .../routes/lib/short_url_assert_valid.test.ts | 8 +- .../routes/lib/short_url_assert_valid.ts | 2 +- .../public/helpers/find_test_subject.ts | 4 +- src/test_utils/public/testing_lib_helpers.tsx | 22 + test/functional/apps/context/_date_nanos.js | 1 + .../context/_date_nanos_custom_timestamp.js | 1 + test/functional/apps/context/_filters.js | 6 + test/functional/apps/context/index.js | 5 +- .../apps/dashboard/dashboard_state.js | 15 + .../apps/dashboard/dashboard_time_picker.js | 2 +- .../apps/dashboard/panel_context_menu.ts | 14 + test/functional/apps/discover/_date_nanos.js | 5 +- .../apps/discover/_date_nanos_mixed.js | 5 +- test/functional/apps/discover/_discover.js | 23 +- .../apps/discover/_discover_histogram.ts | 1 + .../apps/discover/_doc_navigation.js | 4 + test/functional/apps/discover/_doc_table.ts | 1 + test/functional/apps/discover/_errors.ts | 4 + test/functional/apps/discover/_field_data.js | 1 + .../apps/discover/_field_visualize.ts | 1 + .../apps/discover/_filter_editor.js | 1 + .../discover/_indexpattern_with_encoded_id.ts | 5 +- .../_indexpattern_without_timefield.ts | 5 +- test/functional/apps/discover/_inspector.js | 1 + .../functional/apps/discover/_large_string.js | 5 +- .../apps/discover/_saved_queries.js | 4 +- .../functional/apps/discover/_shared_links.js | 9 +- test/functional/apps/discover/_sidebar.js | 11 +- .../apps/discover/_source_filters.js | 11 +- test/functional/apps/home/_navigation.ts | 3 + .../apps/management/_handle_alias.js | 4 + .../apps/management/_scripted_fields.js | 5 +- test/functional/apps/visualize/_lab_mode.js | 6 + test/functional/apps/visualize/index.ts | 1 + test/functional/config.js | 2 +- .../services/dashboard/visualizations.ts | 9 + 405 files changed, 16091 insertions(+), 793 deletions(-) create mode 100644 src/plugins/data_explorer/.i18nrc.json create mode 100755 src/plugins/data_explorer/README.md create mode 100644 src/plugins/data_explorer/common/index.ts create mode 100644 src/plugins/data_explorer/opensearch_dashboards.json create mode 100644 src/plugins/data_explorer/public/application.tsx create mode 100644 src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap create mode 100644 src/plugins/data_explorer/public/components/app.tsx create mode 100644 src/plugins/data_explorer/public/components/app_container.scss create mode 100644 src/plugins/data_explorer/public/components/app_container.test.tsx create mode 100644 src/plugins/data_explorer/public/components/app_container.tsx create mode 100644 src/plugins/data_explorer/public/components/no_view.tsx create mode 100644 src/plugins/data_explorer/public/components/sidebar/index.tsx create mode 100644 src/plugins/data_explorer/public/index.scss create mode 100644 src/plugins/data_explorer/public/index.ts create mode 100644 src/plugins/data_explorer/public/plugin.ts create mode 100644 src/plugins/data_explorer/public/services/view_service/index.ts create mode 100644 src/plugins/data_explorer/public/services/view_service/types.ts create mode 100644 src/plugins/data_explorer/public/services/view_service/view.ts create mode 100644 src/plugins/data_explorer/public/services/view_service/view_service.test.ts create mode 100644 src/plugins/data_explorer/public/services/view_service/view_service.ts create mode 100644 src/plugins/data_explorer/public/types.ts create mode 100644 src/plugins/data_explorer/public/utils/mocks.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/hooks.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/index.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/preload.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx create mode 100644 src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/store.ts create mode 100644 src/plugins/data_explorer/public/utils/use/index.ts create mode 100644 src/plugins/data_explorer/public/utils/use/use_view.ts create mode 100644 src/plugins/data_explorer/server/index.ts create mode 100644 src/plugins/data_explorer/server/plugin.ts create mode 100644 src/plugins/data_explorer/server/routes/index.ts create mode 100644 src/plugins/data_explorer/server/types.ts create mode 100644 src/plugins/discover/public/__mock__/index_pattern_mock.ts create mode 100644 src/plugins/discover/public/application/components/chart/chart.tsx create mode 100644 src/plugins/discover/public/application/components/chart/histogram/histogram.tsx create mode 100644 src/plugins/discover/public/application/components/chart/histogram/type.ts rename src/plugins/discover/public/application/components/{ => chart}/hits_counter/hits_counter.test.tsx (100%) create mode 100644 src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.tsx rename src/plugins/discover/public/application/components/{ => chart}/hits_counter/index.ts (100%) create mode 100644 src/plugins/discover/public/application/components/chart/timechart_header/index.ts rename src/plugins/discover/public/application/components/{ => chart}/timechart_header/timechart_header.test.tsx (100%) create mode 100644 src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx create mode 100644 src/plugins/discover/public/application/components/chart/utils/create_histogram_configs.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/get_dimension.test.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/get_dimensions.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/index.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/point_series.test.ts create mode 100644 src/plugins/discover/public/application/components/chart/utils/point_series.ts create mode 100644 src/plugins/discover/public/application/components/data_grid/constants.ts create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.test.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.test.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_inspect_button.tsx create mode 100644 src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx create mode 100644 src/plugins/discover/public/application/components/help_menu/help_menu_util.ts create mode 100644 src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss create mode 100644 src/plugins/discover/public/application/components/no_results/no_results.tsx create mode 100644 src/plugins/discover/public/application/components/table/table.scss rename src/plugins/discover/public/application/components/top_nav/__snapshots__/{open_search_panel.test.js.snap => open_search_panel.test.tsx.snap} (100%) create mode 100644 src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx create mode 100644 src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx create mode 100644 src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx create mode 100644 src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx rename src/plugins/discover/public/application/{angular/directives => components/uninitialized}/uninitialized.tsx (100%) create mode 100644 src/plugins/discover/public/application/components/utils/use_pagination.test.ts create mode 100644 src/plugins/discover/public/application/components/utils/use_pagination.ts create mode 100644 src/plugins/discover/public/application/utils/columns.test.ts create mode 100644 src/plugins/discover/public/application/utils/columns.ts create mode 100644 src/plugins/discover/public/application/utils/state_management/common.test.ts create mode 100644 src/plugins/discover/public/application/utils/state_management/common.ts create mode 100644 src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx create mode 100644 src/plugins/discover/public/application/utils/state_management/discover_slice.tsx create mode 100644 src/plugins/discover/public/application/utils/state_management/index.ts create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_chart_container.scss create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/index.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/top_nav.tsx create mode 100644 src/plugins/discover/public/application/view_components/context/index.tsx create mode 100644 src/plugins/discover/public/application/view_components/index.ts create mode 100644 src/plugins/discover/public/application/view_components/panel/index.tsx create mode 100644 src/plugins/discover/public/application/view_components/utils/get_default_sort.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/get_sort.test.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/get_sort.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/update_search_source.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/use_search.ts create mode 100644 src/plugins/discover/public/migrate_state.ts create mode 100644 src/plugins/discover_legacy/README.md create mode 100644 src/plugins/discover_legacy/common/index.ts create mode 100644 src/plugins/discover_legacy/opensearch_dashboards.json rename src/plugins/{discover => discover_legacy}/public/application/_discover.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/NOTES.md (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/_stubs.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/anchor.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/anchor.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/context.predecessors.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/context.successors.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/context.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/date_conversion.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/date_conversion.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/fetch_hits_in_interval.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/generate_intervals.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/get_opensearch_query_search_after.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/get_opensearch_query_sort.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/sorting.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/api/utils/sorting.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/_action_bar.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/action_bar.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/action_bar.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/action_bar_directive.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/action_bar_warning.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/components/action_bar/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/helpers/call_after_bindings_workaround.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query/actions.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query/constants.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query/index.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query/state.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query_parameters/actions.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query_parameters/actions.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query_parameters/constants.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query_parameters/index.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context/query_parameters/state.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context_app.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context_app.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context_state.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/context_state.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/__snapshots__/no_results.test.js.snap (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/_histogram.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/_no_results.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/debounce/debounce.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/debounce/debounce.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/debounce/index.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/fixed_scroll.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/fixed_scroll.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/histogram.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/index.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/no_results.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/no_results.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/directives/render_complete.ts (100%) create mode 100644 src/plugins/discover_legacy/public/application/angular/directives/uninitialized.tsx rename src/plugins/{discover => discover_legacy}/public/application/angular/discover.js (98%) rename src/plugins/{discover => discover_legacy}/public/application/angular/discover_legacy.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/discover_state.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/discover_state.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/_doc_table.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/actions/columns.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/_table_header.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/row_headers.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header/helpers.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header/table_header.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header/table_header.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_header/table_header_column.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/_cell.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/_details.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/_index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/_open.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/cell.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/details.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/open.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/components/table_row/truncate_by_height.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/create_doc_table_react.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/doc_table.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/doc_table.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/doc_table.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/doc_table_strings.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/index.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/infinite_scroll.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/get_default_sort.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/get_sort.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/get_sort.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/get_sort_for_search_source.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/pager/index.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/pager/pager.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_table/lib/pager/pager_factory.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_viewer.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/doc_viewer_links.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/helpers/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/helpers/point_series.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/redirect.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/angular/response_handler.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/application.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/context_error_message/context_error_message.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/context_error_message/context_error_message.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/context_error_message/context_error_message_directive.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/context_error_message/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/create_discover_legacy_directive.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/discover_legacy.tsx (95%) create mode 100644 src/plugins/discover_legacy/public/application/components/doc/doc.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc/doc.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap create mode 100644 src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts rename src/plugins/{discover => discover_legacy}/public/application/components/help_menu/help_menu_util.js (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/hits_counter/hits_counter.tsx (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/hits_counter/index.ts create mode 100644 src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap create mode 100644 src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/change_indexpattern.tsx (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/discover_field_details.scss (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/discover_index_pattern.test.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/discover_index_pattern.tsx (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/sidebar/discover_index_pattern_title.tsx (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/index.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/sidebar/types.ts create mode 100644 src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts create mode 100644 src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table.test.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_helper.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx create mode 100644 src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/timechart_header/index.ts (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx rename src/plugins/{discover => discover_legacy}/public/application/components/timechart_header/timechart_header.tsx (100%) create mode 100644 src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap rename src/plugins/{discover => discover_legacy}/public/application/components/top_nav/open_search_panel.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/top_nav/open_search_panel.test.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/components/top_nav/show_open_search_panel.js (100%) rename src/plugins/{discover => discover_legacy}/public/application/doc_views/doc_views_helpers.tsx (100%) create mode 100644 src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts create mode 100644 src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts create mode 100644 src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts create mode 100644 src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts rename src/plugins/{discover => discover_legacy}/public/application/embeddable/constants.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/index.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/search_embeddable.scss (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/search_embeddable.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/search_embeddable_factory.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/search_template.html (100%) rename src/plugins/{discover => discover_legacy}/public/application/embeddable/types.ts (100%) create mode 100644 src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts rename src/plugins/{discover => discover_legacy}/public/application/helpers/get_switch_index_pattern_app_state.test.ts (100%) rename src/plugins/{discover => discover_legacy}/public/application/helpers/get_switch_index_pattern_app_state.ts (100%) create mode 100644 src/plugins/discover_legacy/public/application/helpers/index.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/popularize_field.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts create mode 100644 src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts rename src/plugins/{discover => discover_legacy}/public/application/index.scss (100%) create mode 100644 src/plugins/discover_legacy/public/build_services.ts rename src/plugins/{discover => discover_legacy}/public/get_inner_angular.ts (100%) create mode 100644 src/plugins/discover_legacy/public/index.ts create mode 100644 src/plugins/discover_legacy/public/mocks.ts create mode 100644 src/plugins/discover_legacy/public/opensearch_dashboards_services.ts create mode 100644 src/plugins/discover_legacy/public/plugin.ts create mode 100644 src/plugins/discover_legacy/public/saved_searches/_saved_search.ts create mode 100644 src/plugins/discover_legacy/public/saved_searches/index.ts create mode 100644 src/plugins/discover_legacy/public/saved_searches/saved_searches.ts create mode 100644 src/plugins/discover_legacy/public/saved_searches/types.ts create mode 100644 src/plugins/discover_legacy/public/url_generator.test.ts create mode 100644 src/plugins/discover_legacy/public/url_generator.ts create mode 100644 src/test_utils/public/testing_lib_helpers.tsx diff --git a/packages/osd-stylelint-config/config/global_selectors.json b/packages/osd-stylelint-config/config/global_selectors.json index 19451066878c..99b2db2dfb4f 100644 --- a/packages/osd-stylelint-config/config/global_selectors.json +++ b/packages/osd-stylelint-config/config/global_selectors.json @@ -23,8 +23,8 @@ "src/plugins/vis_builder/public/application/components/searchable_dropdown.scss", "src/plugins/vis_builder/public/application/components/side_nav.scss", "packages/osd-ui-framework/src/components/button/button_group/_button_group.scss", - "src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss", - "src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss" + "src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss", + "src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_open.scss" ] } -} +} \ No newline at end of file diff --git a/packages/osd-stylelint-config/config/restricted_properties.json b/packages/osd-stylelint-config/config/restricted_properties.json index b69012cb61f9..d229764c8d88 100644 --- a/packages/osd-stylelint-config/config/restricted_properties.json +++ b/packages/osd-stylelint-config/config/restricted_properties.json @@ -2,7 +2,7 @@ "font-family": { "explanation": "All \"font-family\" styles should be inherited from OUI themes and components. Remove the rule.", "approved": [ - "src/plugins/discover/public/application/_discover.scss", + "src/plugins/discover_legacy/public/application/_discover.scss", "src/plugins/maps_legacy/public/map/_leaflet_overrides.scss", "src/plugins/maps_legacy/public/map/_legend.scss", "src/plugins/opensearch_dashboards_legacy/public/font_awesome/font_awesome.scss", @@ -12,7 +12,8 @@ "src/plugins/data/public/ui/typeahead/_suggestion.scss", "src/plugins/vis_type_timeseries/public/application/components/_error.scss", "packages/osd-ui-framework/src/components/form/check_box/_check_box.scss", - "src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss" + "src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss", + "src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss" ] } -} +} \ No newline at end of file diff --git a/src/plugins/data_explorer/.i18nrc.json b/src/plugins/data_explorer/.i18nrc.json new file mode 100644 index 000000000000..1ea4ccdd1e7b --- /dev/null +++ b/src/plugins/data_explorer/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "dataExplorer", + "paths": { + "dataExplorer": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/src/plugins/data_explorer/README.md b/src/plugins/data_explorer/README.md new file mode 100755 index 000000000000..8ea14c17d428 --- /dev/null +++ b/src/plugins/data_explorer/README.md @@ -0,0 +1,11 @@ +# dataExplorer + +A OpenSearch Dashboards plugin + +--- + +## Development + +See the [OpenSearch Dashboards contributing +guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/CONTRIBUTING.md) for instructions +setting up your development environment. diff --git a/src/plugins/data_explorer/common/index.ts b/src/plugins/data_explorer/common/index.ts new file mode 100644 index 000000000000..60d61f38b749 --- /dev/null +++ b/src/plugins/data_explorer/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const PLUGIN_ID = 'data-explorer'; +export const PLUGIN_NAME = 'Data Explorer'; diff --git a/src/plugins/data_explorer/opensearch_dashboards.json b/src/plugins/data_explorer/opensearch_dashboards.json new file mode 100644 index 000000000000..23db353b2cc8 --- /dev/null +++ b/src/plugins/data_explorer/opensearch_dashboards.json @@ -0,0 +1,18 @@ +{ + "id": "dataExplorer", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [ + "data", + "navigation", + "embeddable", + "expressions" + ], + "optionalPlugins": [], + "requiredBundles": [ + "opensearchDashboardsReact", + "opensearchDashboardsUtils" + ] +} \ No newline at end of file diff --git a/src/plugins/data_explorer/public/application.tsx b/src/plugins/data_explorer/public/application.tsx new file mode 100644 index 000000000000..ae57070f7451 --- /dev/null +++ b/src/plugins/data_explorer/public/application.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Router, Route, Switch } from 'react-router-dom'; +import { AppMountParameters, CoreStart } from '../../../core/public'; +import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public'; +import { DataExplorerServices } from './types'; +import { DataExplorerApp } from './components/app'; +import { Store } from './utils/state_management'; + +export const renderApp = ( + core: CoreStart, + services: DataExplorerServices, + params: AppMountParameters, + store: Store +) => { + const { history, element } = params; + ReactDOM.render( + + + + + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap b/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap new file mode 100644 index 000000000000..29b8e5ab54e9 --- /dev/null +++ b/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataExplorerApp should render the canvas and panel when selected 1`] = ` +
+
+
+ Context +
+
+
+`; diff --git a/src/plugins/data_explorer/public/components/app.tsx b/src/plugins/data_explorer/public/components/app.tsx new file mode 100644 index 000000000000..ff6b5931a404 --- /dev/null +++ b/src/plugins/data_explorer/public/components/app.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { AppMountParameters } from '../../../../core/public'; +import { useView } from '../utils/use'; +import { AppContainer } from './app_container'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { DataExplorerServices } from '../types'; +import { syncQueryStateWithUrl } from '../../../data/public'; + +export const DataExplorerApp = ({ params }: { params: AppMountParameters }) => { + const { view } = useView(); + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); + + return ; +}; diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss new file mode 100644 index 000000000000..d289e7d4be3e --- /dev/null +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -0,0 +1,18 @@ +$osdHeaderOffset: $euiHeaderHeightCompensation; + +.deSidebar { + max-width: 462px; + min-width: 400px; +} + +.deLayout { + height: calc(100vh - #{$osdHeaderOffset}); + + &__canvas { + height: 100%; + } +} + +.headerIsExpanded .deLayout { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} diff --git a/src/plugins/data_explorer/public/components/app_container.test.tsx b/src/plugins/data_explorer/public/components/app_container.test.tsx new file mode 100644 index 000000000000..14175369490c --- /dev/null +++ b/src/plugins/data_explorer/public/components/app_container.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { AppContainer } from './app_container'; +import { View } from '../services/view_service/view'; +import { AppMountParameters } from '../../../../core/public'; +import { render } from 'test_utils/testing_lib_helpers'; + +describe('DataExplorerApp', () => { + const createView = () => { + return new View({ + id: 'test-view', + title: 'Test View', + defaultPath: '/test-path', + appExtentions: {} as any, + Canvas: (() =>
canvas
) as any, + Panel: (() =>
panel
) as any, + Context: (() =>
Context
) as any, + }); + }; + + const params: AppMountParameters = { + element: document.createElement('div'), + history: {} as any, + onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), + appBasePath: '', + }; + + it('should render NoView when a non existent view is selected', () => { + const { container } = render(); + + expect(container).toContainHTML('View not found'); + }); + + it('should render the canvas and panel when selected', () => { + const view = createView(); + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx new file mode 100644 index 000000000000..8f37e9c1230f --- /dev/null +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiPage, EuiPageBody } from '@elastic/eui'; +import { Suspense } from 'react'; +import { AppMountParameters } from '../../../../core/public'; +import { Sidebar } from './sidebar'; +import { NoView } from './no_view'; +import { View } from '../services/view_service/view'; +import './app_container.scss'; + +export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { + // TODO: Make this more robust. + if (!view) { + return ; + } + + const { Canvas, Panel, Context } = view; + + // Render the application DOM. + return ( + + {/* TODO: improve fallback state */} + Loading...}> + + + + + + + + + + + ); +}; diff --git a/src/plugins/data_explorer/public/components/no_view.tsx b/src/plugins/data_explorer/public/components/no_view.tsx new file mode 100644 index 000000000000..a341e9d0564e --- /dev/null +++ b/src/plugins/data_explorer/public/components/no_view.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiPageTemplate, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +export const NoView = () => { + return ( + + + + + } + body={ +

+ +

+ } + /> +
+ ); +}; diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx new file mode 100644 index 000000000000..579c024acbc7 --- /dev/null +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -0,0 +1,120 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, FC, useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiComboBox, + EuiSelect, + EuiComboBoxOptionOption, + EuiSpacer, + EuiSplitPanel, + EuiPageSideBar, +} from '@elastic/eui'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { useView } from '../../utils/use'; +import { DataExplorerServices } from '../../types'; +import { useTypedDispatch, useTypedSelector, setIndexPattern } from '../../utils/state_management'; +import { setView } from '../../utils/state_management/metadata_slice'; + +export const Sidebar: FC = ({ children }) => { + const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata); + const dispatch = useTypedDispatch(); + const [options, setOptions] = useState>>([]); + const [selectedOption, setSelectedOption] = useState>(); + const { view, viewRegistry } = useView(); + const views = viewRegistry.all(); + const viewOptions = useMemo( + () => + views.map(({ id, title }) => ({ + value: id, + text: title, + })), + [views] + ); + + const { + services: { + data: { indexPatterns }, + notifications: { toasts }, + }, + } = useOpenSearchDashboards(); + + useEffect(() => { + let isMounted = true; + const fetchIndexPatterns = async () => { + await indexPatterns.ensureDefaultIndexPattern(); + const cache = await indexPatterns.getCache(); + const currentOptions = (cache || []).map((indexPattern) => ({ + label: indexPattern.attributes.title, + value: indexPattern.id, + })); + if (isMounted) { + setOptions(currentOptions); + } + }; + fetchIndexPatterns(); + + return () => { + isMounted = false; + }; + }, [indexPatterns]); + + // Set option to the current index pattern + useEffect(() => { + if (indexPatternId) { + const option = options.find((o) => o.value === indexPatternId); + setSelectedOption(option); + } + }, [indexPatternId, options]); + + return ( + + + + { + // TODO: There are many issues with this approach, but it's a start + // 1. Combo box can delete a selected index pattern. This should not be possible + // 2. Combo box is severely truncated. This should be fixed in the EUI component + // 3. The onchange can fire with a option that is not valid. discuss where to handle this. + // 4. value is optional. If the combobox needs to act as a slecet, this should be required. + const { value } = selected[0] || {}; + + if (!value) { + toasts.addWarning({ + id: 'index-pattern-not-found', + title: i18n.translate('dataExplorer.indexPatternError', { + defaultMessage: 'Index pattern not found', + }), + }); + return; + } + + dispatch(setIndexPattern(value)); + }} + /> + + { + dispatch(setView(e.target.value)); + }} + fullWidth + /> + + + {children} + + + + ); +}; diff --git a/src/plugins/data_explorer/public/index.scss b/src/plugins/data_explorer/public/index.scss new file mode 100644 index 000000000000..8389e31b426a --- /dev/null +++ b/src/plugins/data_explorer/public/index.scss @@ -0,0 +1,9 @@ +$osdHeaderOffset: $euiHeaderHeightCompensation; + +.dePageTemplate { + height: calc(100vh - #{$osdHeaderOffset}); +} + +.headerIsExpanded .dePageTemplate { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts new file mode 100644 index 000000000000..635a0ec285db --- /dev/null +++ b/src/plugins/data_explorer/public/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './index.scss'; + +import { DataExplorerPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export function plugin() { + return new DataExplorerPlugin(); +} +export { DataExplorerPluginSetup, DataExplorerPluginStart, DataExplorerServices } from './types'; +export { ViewProps, ViewDefinition, DefaultViewState } from './services/view_service'; +export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management'; diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts new file mode 100644 index 000000000000..3b953567fbe8 --- /dev/null +++ b/src/plugins/data_explorer/public/plugin.ts @@ -0,0 +1,140 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + AppNavLinkStatus, + ScopedHistory, + AppUpdater, +} from '../../../core/public'; +import { + DataExplorerPluginSetup, + DataExplorerPluginSetupDependencies, + DataExplorerPluginStart, + DataExplorerPluginStartDependencies, + DataExplorerServices, +} from './types'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; +import { ViewService } from './services/view_service'; +import { + createOsdUrlStateStorage, + createOsdUrlTracker, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; +import { getPreloadedStore } from './utils/state_management'; +import { opensearchFilters } from '../../data/public'; + +export class DataExplorerPlugin + implements + Plugin< + DataExplorerPluginSetup, + DataExplorerPluginStart, + DataExplorerPluginSetupDependencies, + DataExplorerPluginStartDependencies + > { + private viewService = new ViewService(); + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking?: () => void; + private currentHistory?: ScopedHistory; + + public setup( + core: CoreSetup, + { data }: DataExplorerPluginSetupDependencies + ): DataExplorerPluginSetup { + const viewService = this.viewService; + + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:${PLUGIN_ID}`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + // Register an application into the side navigation menu + core.application.register({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + // Load application bundle + const { renderApp } = await import('./application'); + + const [coreStart, pluginsStart] = await core.getStartServices(); + this.currentHistory = params.history; + + // make sure the index pattern list is up to date + pluginsStart.data.indexPatterns.clearCache(); + + const services: DataExplorerServices = { + ...coreStart, + scopedHistory: this.currentHistory, + data: pluginsStart.data, + embeddable: pluginsStart.embeddable, + expressions: pluginsStart.expressions, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: this.currentHistory, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), + viewRegistry: viewService.start(), + }; + + // Get start services as specified in opensearch_dashboards.json + // Render the application + const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); + services.store = store; + + const unmount = renderApp(coreStart, services, params, store); + appMounted(); + + return () => { + unsubscribeStore(); + appUnMounted(); + unmount(); + }; + }, + }); + + return { + ...this.viewService.setup(), + }; + } + + public start(core: CoreStart): DataExplorerPluginStart { + return {}; + } + + public stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } +} diff --git a/src/plugins/data_explorer/public/services/view_service/index.ts b/src/plugins/data_explorer/public/services/view_service/index.ts new file mode 100644 index 000000000000..06bfe5c341f7 --- /dev/null +++ b/src/plugins/data_explorer/public/services/view_service/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './view_service'; +export * from './types'; diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts new file mode 100644 index 000000000000..5ecba7920b63 --- /dev/null +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Slice } from '@reduxjs/toolkit'; +import { LazyExoticComponent } from 'react'; +import { AppMountParameters } from '../../../../../core/public'; +import { RootState } from '../../utils/state_management'; + +interface ViewListItem { + id: string; + label: string; +} + +export interface DefaultViewState { + state: T; + root?: Partial; +} + +export type ViewProps = AppMountParameters; + +export interface ViewDefinition { + readonly id: string; + readonly title: string; + readonly ui?: { + defaults: DefaultViewState | (() => DefaultViewState) | (() => Promise); + slice: Slice; + }; + readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; + readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; + readonly Context: LazyExoticComponent< + (props: React.PropsWithChildren) => React.ReactElement + >; + readonly defaultPath: string; + readonly appExtentions: { + savedObject: { + docTypes: [string]; + toListItem: (obj: { id: string; title: string }) => ViewListItem; + }; + }; + readonly shouldShow?: (state: any) => boolean; +} diff --git a/src/plugins/data_explorer/public/services/view_service/view.ts b/src/plugins/data_explorer/public/services/view_service/view.ts new file mode 100644 index 000000000000..ebdab31fddc5 --- /dev/null +++ b/src/plugins/data_explorer/public/services/view_service/view.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { ViewDefinition } from './types'; + +type IView = ViewDefinition; + +export class View implements IView { + public readonly id: string; + public readonly title: string; + public readonly ui: IView['ui']; + public readonly defaultPath: string; + public readonly appExtentions: IView['appExtentions']; + readonly shouldShow?: (state: any) => boolean; + readonly Canvas: IView['Canvas']; + readonly Panel: IView['Panel']; + readonly Context: IView['Context']; + + constructor(options: ViewDefinition) { + this.id = options.id; + this.title = options.title; + this.ui = options.ui; + this.defaultPath = options.defaultPath; + this.appExtentions = options.appExtentions; + this.shouldShow = options.shouldShow; + this.Canvas = options.Canvas; + this.Panel = options.Panel; + this.Context = options.Context; + } +} diff --git a/src/plugins/data_explorer/public/services/view_service/view_service.test.ts b/src/plugins/data_explorer/public/services/view_service/view_service.test.ts new file mode 100644 index 000000000000..7bbf486a9991 --- /dev/null +++ b/src/plugins/data_explorer/public/services/view_service/view_service.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ViewDefinition } from './types'; +import { ViewService } from './view_service'; + +const DEFAULT_VIEW: ViewDefinition = { + id: 'my-view', + title: 'My view', + defaultPath: '/my-view', + appExtentions: {} as any, // Not required for this test + ui: {} as any, // Not required for this test +}; + +describe('TypeService', () => { + const createViewDefinition = (props?: Partial): ViewDefinition => { + return { + ...DEFAULT_VIEW, + ...props, + }; + }; + + let service: ViewService; + + beforeEach(() => { + service = new ViewService(); + }); + + describe('#setup', () => { + test('should throw an error if two visualizations of the same id are registered', () => { + const { registerView } = service.setup(); + + registerView(createViewDefinition({ id: 'view-1' })); + + expect(() => { + registerView(createViewDefinition({ id: 'view-1' })); + }).toThrowErrorMatchingInlineSnapshot(`"A view with the id view-1 already exists!"`); + }); + }); + + describe('#start', () => { + test('should return registered view if it exists', () => { + const { registerView } = service.setup(); + registerView(createViewDefinition({ id: 'view-1' })); + + const { get } = service.start(); + expect(get('view-1')).toEqual(expect.objectContaining({ id: 'view-1' })); + expect(get('view-something')).toBeUndefined(); + }); + + test('should return all registered views', () => { + const { registerView } = service.setup(); + registerView(createViewDefinition({ id: 'view-1' })); + registerView(createViewDefinition({ id: 'view-2' })); + + const { all } = service.start(); + const allRegisteredViews = all(); + expect(allRegisteredViews.map(({ id }) => id)).toEqual(['view-1', 'view-2']); + }); + }); +}); diff --git a/src/plugins/data_explorer/public/services/view_service/view_service.ts b/src/plugins/data_explorer/public/services/view_service/view_service.ts new file mode 100644 index 000000000000..0ee131df0665 --- /dev/null +++ b/src/plugins/data_explorer/public/services/view_service/view_service.ts @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreService } from '../../../../../core/types'; +import { ViewDefinition } from './types'; +import { View } from './view'; + +/** + * Visualization Types Service + * + * @internal + */ +export class ViewService implements CoreService { + private views: Record = {}; + + private registerView(view: View) { + if (view.id in this.views) { + throw new Error(`A view with the id ${view.id} already exists!`); + } + this.views[view.id] = view; + } + + public setup() { + return { + /** + * registers a visualization type + * @param config - visualization type definition + */ + registerView: (config: ViewDefinition): void => { + const view = new View(config); + this.registerView(view); + }, + }; + } + + public start() { + return { + /** + * returns specific View or undefined if not found + * @param {string} id - id of view to return + */ + get: (id: string): View | undefined => { + return this.views[id]; + }, + /** + * returns all registered Views + */ + all: (): View[] => { + return Object.values(this.views); + }, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @internal */ +export type ViewServiceSetup = ReturnType; +export type ViewServiceStart = ReturnType; diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts new file mode 100644 index 000000000000..5f677fb46cfd --- /dev/null +++ b/src/plugins/data_explorer/public/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart, ScopedHistory } from 'opensearch-dashboards/public'; +import { EmbeddableStart } from '../../embeddable/public'; +import { ExpressionsStart } from '../../expressions/public'; +import { ViewServiceStart, ViewServiceSetup } from './services/view_service'; +import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; +import { Store } from './utils/state_management'; + +export type DataExplorerPluginSetup = ViewServiceSetup; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataExplorerPluginStart {} + +export interface DataExplorerPluginSetupDependencies { + data: DataPublicPluginSetup; +} + +export interface DataExplorerPluginStartDependencies { + expressions: ExpressionsStart; + embeddable: EmbeddableStart; + data: DataPublicPluginStart; +} + +export interface DataExplorerServices extends CoreStart { + store?: Store; + viewRegistry: ViewServiceStart; + expressions: ExpressionsStart; + embeddable: EmbeddableStart; + data: DataPublicPluginStart; + scopedHistory: ScopedHistory; + osdUrlStateStorage: IOsdUrlStateStorage; +} diff --git a/src/plugins/data_explorer/public/utils/mocks.ts b/src/plugins/data_explorer/public/utils/mocks.ts new file mode 100644 index 000000000000..9604a0ba8dc6 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/mocks.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ScopedHistory } from '../../../../core/public'; +import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { embeddablePluginMock } from '../../../embeddable/public/mocks'; +import { expressionsPluginMock } from '../../../expressions/public/mocks'; +import { createOsdUrlStateStorage } from '../../../opensearch_dashboards_utils/public'; +import { DataExplorerServices } from '../types'; + +export const createDataExplorerServicesMock = () => { + const coreStartMock = coreMock.createStart(); + const dataMock = dataPluginMock.createStartContract(); + const embeddableMock = embeddablePluginMock.createStartContract(); + const expressionMock = expressionsPluginMock.createStartContract(); + const osdUrlStateStorageMock = createOsdUrlStateStorage({ useHash: false }); + + const dataExplorerServicesMock: DataExplorerServices = { + ...coreStartMock, + expressions: expressionMock, + data: dataMock, + osdUrlStateStorage: osdUrlStateStorageMock, + embeddable: embeddableMock, + scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory, + viewRegistry: { + get: jest.fn(), + all: jest.fn(() => []), + }, + }; + + return (dataExplorerServicesMock as unknown) as jest.Mocked; +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/hooks.ts b/src/plugins/data_explorer/public/utils/state_management/hooks.ts new file mode 100644 index 000000000000..d4194da3702f --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/hooks.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout the app instead of plain `useDispatch` and `useSelector` +export const useTypedDispatch = () => useDispatch(); +export const useTypedSelector: ( + selector: (state: TState) => TSelected, + equalityFn?: (left: TSelected, right: TSelected) => boolean +) => TSelected = useSelector; diff --git a/src/plugins/data_explorer/public/utils/state_management/index.ts b/src/plugins/data_explorer/public/utils/state_management/index.ts new file mode 100644 index 000000000000..edb5c2a17184 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store'; +export * from './hooks'; diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts new file mode 100644 index 000000000000..e9fe84713120 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { DataExplorerServices } from '../../types'; + +export interface MetadataState { + indexPattern?: string; + originatingApp?: string; + view?: string; +} + +const initialState: MetadataState = {}; + +export const getPreloadedState = async ({ + embeddable, + scopedHistory, + data, +}: DataExplorerServices): Promise => { + const { originatingApp } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const preloadedState: MetadataState = { + ...initialState, + originatingApp, + indexPattern: defaultIndexPattern?.id, + }; + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'metadata', + initialState, + reducers: { + setIndexPattern: (state, action: PayloadAction) => { + state.indexPattern = action.payload; + }, + setOriginatingApp: (state, action: PayloadAction) => { + state.originatingApp = action.payload; + }, + setView: (state, action: PayloadAction) => { + state.view = action.payload; + }, + setState: (_state, action: PayloadAction) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setIndexPattern, setOriginatingApp, setView, setState } = slice.actions; diff --git a/src/plugins/data_explorer/public/utils/state_management/preload.ts b/src/plugins/data_explorer/public/utils/state_management/preload.ts new file mode 100644 index 000000000000..fe5c23bd366c --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PreloadedState } from '@reduxjs/toolkit'; +import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; +import { RootState } from './store'; +import { DataExplorerServices } from '../../types'; + +export const getPreloadedState = async ( + services: DataExplorerServices +): Promise> => { + let rootState: RootState = { + metadata: await getPreloadedMetadataState(services), + }; + + // initialize the default state for each view + const views = services.viewRegistry.all(); + const promises = views.map(async (view) => { + if (!view.ui) { + return; + } + + const { defaults } = view.ui; + + try { + // defaults can be a function or an object + const preloadedState = typeof defaults === 'function' ? await defaults() : defaults; + rootState[view.id] = preloadedState.state; + + // if the view wants to override the root state, we do that here + if (preloadedState.root) { + rootState = { + ...rootState, + ...preloadedState.root, + }; + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Error initializing view ${view.id}: ${e}`); + } + }); + await Promise.all(promises); + + return rootState; +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx new file mode 100644 index 000000000000..62159558a0c4 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataExplorerServices } from '../../types'; +import { createDataExplorerServicesMock } from '../mocks'; +import { loadReduxState, persistReduxState } from './redux_persistence'; + +describe('test redux state persistence', () => { + let mockServices: jest.Mocked; + let reduxStateParams: any; + + beforeEach(() => { + mockServices = createDataExplorerServicesMock(); + reduxStateParams = { + discover: 'visualization', + metadata: 'metadata', + }; + }); + + test('test load default redux state when url is empty', async () => { + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toMatchInlineSnapshot(` + Object { + "metadata": Object { + "indexPattern": "id", + "originatingApp": undefined, + }, + } + `); + }); + + test('test load redux state', async () => { + mockServices.osdUrlStateStorage.set('_a', reduxStateParams, { replace: true }); + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(reduxStateParams); + }); + + test('test persist redux state', () => { + persistReduxState(reduxStateParams, mockServices); + const urlStates = mockServices.osdUrlStateStorage.get('_a'); + expect(urlStates).toStrictEqual(reduxStateParams); + }); +}); diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts new file mode 100644 index 000000000000..81517f3e9f4f --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataExplorerServices } from '../../types'; +import { getPreloadedState } from './preload'; +import { RootState } from './store'; + +export const loadReduxState = async (services: DataExplorerServices) => { + try { + const serializedState = services.osdUrlStateStorage.get('_a'); + if (serializedState !== null) return serializedState; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + + return await getPreloadedState(services); +}; + +export const persistReduxState = (root: RootState, services: DataExplorerServices) => { + try { + services.osdUrlStateStorage.set('_a', root, { + replace: true, + }); + } catch (err) { + return; + } +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts new file mode 100644 index 000000000000..cd967d25fc20 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -0,0 +1,89 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { combineReducers, configureStore, PreloadedState, Reducer, Slice } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { reducer as metadataReducer } from './metadata_slice'; +import { loadReduxState, persistReduxState } from './redux_persistence'; +import { DataExplorerServices } from '../../types'; + +const commonReducers = { + metadata: metadataReducer, +}; + +let dynamicReducers: { + metadata: typeof metadataReducer; + [key: string]: Reducer; +} = { + ...commonReducers, +}; + +const rootReducer = combineReducers(dynamicReducers); + +export const configurePreloadedStore = (preloadedState: PreloadedState) => { + // After registering the slices the root reducer needs to be updated + const updatedRootReducer = combineReducers(dynamicReducers); + + return configureStore({ + reducer: updatedRootReducer, + preloadedState, + }); +}; + +export const getPreloadedStore = async (services: DataExplorerServices) => { + // For each view preload the data and register the slice + const views = services.viewRegistry.all(); + views.forEach((view) => { + if (!view.ui) return; + + const { slice } = view.ui; + registerSlice(slice); + }); + + const preloadedState = await loadReduxState(services); + const store = configurePreloadedStore(preloadedState); + + let previousState = store.getState(); + + // Listen to changes + const handleChange = () => { + const state = store.getState(); + persistReduxState(state, services); + + if (isEqual(state, previousState)) return; + + // Add Side effects here to apply after changes to the store are made. None for now. + + previousState = state; + }; + + // the store subscriber will automatically detect changes and call handleChange function + const unsubscribe = store.subscribe(handleChange); + + const onUnsubscribe = () => { + dynamicReducers = { + ...commonReducers, + }; + + unsubscribe(); + }; + + return { store, unsubscribe: onUnsubscribe }; +}; + +export const registerSlice = (slice: Slice) => { + if (dynamicReducers[slice.name]) { + throw new Error(`Slice ${slice.name} already registered`); + } + dynamicReducers[slice.name] = slice.reducer; +}; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +export type RenderState = Omit; // Remaining state after auxillary states are removed +export type Store = ReturnType; +export type AppDispatch = Store['dispatch']; + +export { MetadataState, setIndexPattern, setOriginatingApp } from './metadata_slice'; diff --git a/src/plugins/data_explorer/public/utils/use/index.ts b/src/plugins/data_explorer/public/utils/use/index.ts new file mode 100644 index 000000000000..baab8736b66c --- /dev/null +++ b/src/plugins/data_explorer/public/utils/use/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './use_view'; diff --git a/src/plugins/data_explorer/public/utils/use/use_view.ts b/src/plugins/data_explorer/public/utils/use/use_view.ts new file mode 100644 index 000000000000..10f67c08907d --- /dev/null +++ b/src/plugins/data_explorer/public/utils/use/use_view.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { DataExplorerServices } from '../../types'; +import { useTypedDispatch, useTypedSelector } from '../state_management'; +import { setView } from '../state_management/metadata_slice'; + +export const useView = () => { + const viewId = useTypedSelector((state) => state.metadata.view); + const { + services: { viewRegistry }, + } = useOpenSearchDashboards(); + const dispatch = useTypedDispatch(); + const { appId } = useParams<{ appId: string }>(); + + const view = useMemo(() => { + if (!viewId) return undefined; + return viewRegistry.get(viewId); + }, [viewId, viewRegistry]); + + useEffect(() => { + const currentView = viewRegistry.get(appId); + + if (!currentView) return; + + dispatch(setView(currentView?.id)); + }, [appId, dispatch, viewRegistry]); + + return { view, viewRegistry }; +}; diff --git a/src/plugins/data_explorer/server/index.ts b/src/plugins/data_explorer/server/index.ts new file mode 100644 index 000000000000..84f443c971fe --- /dev/null +++ b/src/plugins/data_explorer/server/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginInitializerContext } from '../../../core/server'; +import { DataExplorerPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new DataExplorerPlugin(initializerContext); +} + +export { DataExplorerPluginSetup, DataExplorerPluginStart } from './types'; diff --git a/src/plugins/data_explorer/server/plugin.ts b/src/plugins/data_explorer/server/plugin.ts new file mode 100644 index 000000000000..ebc501e2a66d --- /dev/null +++ b/src/plugins/data_explorer/server/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { DataExplorerPluginSetup, DataExplorerPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class DataExplorerPlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('dataExplorer: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('dataExplorer: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/data_explorer/server/routes/index.ts b/src/plugins/data_explorer/server/routes/index.ts new file mode 100644 index 000000000000..a577e27d9fd7 --- /dev/null +++ b/src/plugins/data_explorer/server/routes/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IRouter } from '../../../../core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/data_explorer/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/src/plugins/data_explorer/server/types.ts b/src/plugins/data_explorer/server/types.ts new file mode 100644 index 000000000000..cbe17c922611 --- /dev/null +++ b/src/plugins/data_explorer/server/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataExplorerPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataExplorerPluginStart {} diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 371442385bbf..0cac73333e25 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -1,33 +1,10 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ +export const PLUGIN_ID = 'discover'; +export const NEW_DISCOVER_APP = 'discover:v2'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; diff --git a/src/plugins/discover/opensearch_dashboards.json b/src/plugins/discover/opensearch_dashboards.json index 23e00e7dcdcc..bcbfc2096731 100644 --- a/src/plugins/discover/opensearch_dashboards.json +++ b/src/plugins/discover/opensearch_dashboards.json @@ -6,6 +6,7 @@ "requiredPlugins": [ "charts", "data", + "dataExplorer", "embeddable", "inspector", "opensearchDashboardsLegacy", @@ -16,8 +17,8 @@ ], "optionalPlugins": ["home", "share"], "requiredBundles": [ - "opensearchDashboardsUtils", "home", + "opensearchDashboardsUtils", "savedObjects", "opensearchDashboardsReact" ] diff --git a/src/plugins/discover/public/__mock__/index_pattern_mock.ts b/src/plugins/discover/public/__mock__/index_pattern_mock.ts new file mode 100644 index 000000000000..a8f3bcc2c9dd --- /dev/null +++ b/src/plugins/discover/public/__mock__/index_pattern_mock.ts @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { indexPatterns } from '../../../data/public'; +import { IndexPattern } from '../opensearch_dashboards_services'; +import { IIndexPatternFieldList } from '../../../data/common'; + +// Initial data of index pattern fields +const fieldsData = [ + { + name: '_id', + type: 'string', + scripted: false, + aggregatable: true, + filterable: true, + searchable: true, + sortable: true, + }, + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + aggregatable: true, + searchable: true, + sortable: true, + }, + { + name: '_source', + type: '_source', + scripted: false, + aggregatable: false, + filterable: false, + searchable: false, + sortable: false, + }, +]; + +// Create a mock object for index pattern fields with methods: getAll, getByName and getByType +export const indexPatternFieldMock = { + getAll: () => fieldsData, + getByName: (name) => fieldsData.find((field) => field.name === name), + getByType: (type) => fieldsData.filter((field) => field.type === type), +} as IIndexPatternFieldList; + +// Create a mock for the initial index pattern +export const indexPatternInitialMock = ({ + id: '123', + title: 'test_index', + fields: indexPatternFieldMock, + timeFieldName: 'order_date', + formatHit: jest.fn((hit) => (hit.fields ? hit.fields : hit._source)), + flattenHit: undefined, + formatField: undefined, + metaFields: ['_id', '_index', '_source'], + getFieldByName: jest.fn(() => ({})), +} as unknown) as IndexPattern; + +// Add a flattenHit method to the initial index pattern mock using flattenHitWrapper +const flatternHitMock = indexPatterns.flattenHitWrapper( + indexPatternInitialMock, + indexPatternInitialMock.metaFields +); +indexPatternInitialMock.flattenHit = flatternHitMock; + +// Add a formatField method to the initial index pattern mock +const formatFieldMock = (hit, field) => { + return field === '_source' ? hit._source : indexPatternInitialMock.flattenHit(hit)[field]; +}; +indexPatternInitialMock.formatField = formatFieldMock; + +// Export the fully set up index pattern mock +export const indexPatternMock = indexPatternInitialMock; + +// Export a function that allows customization of index pattern mocks, by adding extra fields to the fieldsData +export const getMockedIndexPatternWithCustomizedFields = (fields) => { + const customizedFieldsData = [...fieldsData, ...fields]; + const customizedFieldsMock = { + getAll: () => customizedFieldsData, + getByName: (name) => customizedFieldsData.find((field) => field.name === name), + getByType: (type) => customizedFieldsData.filter((field) => field.type === type), + } as IIndexPatternFieldList; + + return { + ...indexPatternMock, + fields: customizedFieldsMock, + }; +}; + +// Export a function that allows customization of index pattern mocks with both extra fields and time field +export const getMockedIndexPatternWithTimeField = (fields, timeFiledName: string) => { + const indexPatternWithTimeFieldMock = getMockedIndexPatternWithCustomizedFields(fields); + + return { + ...indexPatternWithTimeFieldMock, + timeFieldName: timeFiledName, + }; +}; diff --git a/src/plugins/discover/public/application/components/chart/chart.tsx b/src/plugins/discover/public/application/components/chart/chart.tsx new file mode 100644 index 000000000000..75e33594fea8 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/chart.tsx @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { IUiSettingsClient } from 'opensearch-dashboards/public'; +import { DataPublicPluginStart, search } from '../../../../../data/public'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader, TimechartHeaderBucketInterval } from './timechart_header'; +import { DiscoverHistogram } from './histogram/histogram'; +import { DiscoverServices } from '../../../build_services'; +import { Chart } from './utils'; + +interface DiscoverChartProps { + bucketInterval: TimechartHeaderBucketInterval; + chartData: Chart; + config: IUiSettingsClient; + data: DataPublicPluginStart; + hits: number; + resetQuery: () => void; + showResetButton?: boolean; + timeField?: string; + services: DiscoverServices; +} + +export const DiscoverChart = ({ + bucketInterval, + chartData, + config, + data, + hits, + resetQuery, + timeField, + services, + showResetButton = false, +}: DiscoverChartProps) => { + const { from, to } = data.query.timefilter.timefilter.getTime(); + const timeRange = { + from: dateMath.parse(from)?.format('YYYY-MM-DDTHH:mm:ss.SSSZ') || '', + to: dateMath.parse(to, { roundUp: true })?.format('YYYY-MM-DDTHH:mm:ss.SSSZ') || '', + }; + + const onChangeInterval = () => {}; + + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + return ( + + + 0 ? hits : 0} + showResetButton={showResetButton} + onResetQuery={resetQuery} + /> + + {timeField && ( + + + + )} + {timeField && chartData && ( + +
+
+ +
+
+
+ )} +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx b/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx new file mode 100644 index 000000000000..51d3f1f1b706 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/histogram/histogram.tsx @@ -0,0 +1,360 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; +import moment from 'moment-timezone'; +import { unitOfTime } from 'moment'; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { euiThemeVars } from '@osd/ui-shared-deps/theme'; + +import { + AnnotationDomainType, + Axis, + Chart, + HistogramBarSeries, + LineAnnotation, + Position, + ScaleType, + Settings, + RectAnnotation, + TooltipValue, + TooltipType, + ElementClickListener, + XYChartElementEvent, + BrushEndListener, + Theme, +} from '@elastic/charts'; + +import { i18n } from '@osd/i18n'; +import { IUiSettingsClient } from 'opensearch-dashboards/public'; +import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; +import { Subscription, combineLatest } from 'rxjs'; +import { Chart as IChart } from '../utils/point_series'; +import { DiscoverServices } from '../../../../build_services'; + +export interface DiscoverHistogramProps { + chartData: IChart; + timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + services: DiscoverServices; +} + +interface DiscoverHistogramState { + chartsTheme: EuiChartThemeType['theme']; + chartsBaseTheme: Theme; +} + +function findIntervalFromDuration( + dateValue: number, + opensearchValue: number, + opensearchUnit: unitOfTime.Base, + timeZone: string +) { + const date = moment.tz(dateValue, timeZone); + const startOfDate = moment.tz(date, timeZone).startOf(opensearchUnit); + const endOfDate = moment + .tz(date, timeZone) + .startOf(opensearchUnit) + .add(opensearchValue, opensearchUnit); + return endOfDate.valueOf() - startOfDate.valueOf(); +} + +function getIntervalInMs( + value: number, + opensearchValue: number, + opensearchUnit: unitOfTime.Base, + timeZone: string +): number { + switch (opensearchUnit) { + case 's': + return 1000 * opensearchValue; + case 'ms': + return 1 * opensearchValue; + default: + return findIntervalFromDuration(value, opensearchValue, opensearchUnit, timeZone); + } +} + +function getTimezone(uiSettings: IUiSettingsClient) { + if (uiSettings.isDefault('dateFormat:tz')) { + const detectedTimezone = moment.tz.guess(); + if (detectedTimezone) return detectedTimezone; + else return moment().format('Z'); + } else { + return uiSettings.get('dateFormat:tz', 'Browser'); + } +} + +export function findMinInterval( + xValues: number[], + opensearchValue: number, + opensearchUnit: string, + timeZone: string +): number { + return xValues.reduce((minInterval, currentXvalue, index) => { + let currentDiff = minInterval; + if (index > 0) { + currentDiff = Math.abs(xValues[index - 1] - currentXvalue); + } + const singleUnitInterval = getIntervalInMs( + currentXvalue, + opensearchValue, + opensearchUnit as unitOfTime.Base, + timeZone + ); + return Math.min(minInterval, singleUnitInterval, currentDiff); + }, Number.MAX_SAFE_INTEGER); +} + +export class DiscoverHistogram extends Component { + public static propTypes = { + chartData: PropTypes.object, + timefilterUpdateHandler: PropTypes.func, + }; + + private subscription?: Subscription; + + componentDidMount() { + this.subscription = combineLatest( + this.props.services.theme.chartsTheme$, + this.props.services.theme.chartsBaseTheme$ + ).subscribe(([chartsTheme, chartsBaseTheme]) => + this.setState({ chartsTheme, chartsBaseTheme }) + ); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public onBrushEnd: BrushEndListener = ({ x }) => { + if (!x) { + return; + } + const [from, to] = x; + this.props.timefilterUpdateHandler({ from, to }); + }; + + public onElementClick = (xInterval: number): ElementClickListener => ([elementData]) => { + const startRange = (elementData as XYChartElementEvent)[0].x; + + const range = { + from: startRange, + to: startRange + xInterval, + }; + + this.props.timefilterUpdateHandler(range); + }; + + public formatXValue = (val: string) => { + const xAxisFormat = this.props.chartData.xAxisFormat.params!.pattern; + + return moment(val).format(xAxisFormat); + }; + + public renderBarTooltip = (xInterval: number, domainStart: number, domainEnd: number) => ( + headerData: TooltipValue + ): JSX.Element | string => { + const headerDataValue = headerData.value; + const formattedValue = this.formatXValue(headerDataValue); + + const partialDataText = i18n.translate('discover.histogram.partialData.bucketTooltipText', { + defaultMessage: + 'The selected time range does not include this entire bucket, it may contain partial data.', + }); + + if (headerDataValue < domainStart || headerDataValue + xInterval > domainEnd) { + return ( + + + + + + {partialDataText} + + +

{formattedValue}

+
+ ); + } + + return formattedValue; + }; + + public render() { + const { chartData, services } = this.props; + const { uiSettings } = services; + const timeZone = getTimezone(uiSettings); + const chartsTheme = services.theme.chartsDefaultTheme; + const chartsBaseTheme = services.theme.chartsDefaultBaseTheme; + + if (!chartData) { + return null; + } + + const data = chartData.values; + + /** + * Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval]. + * see https://github.com/elastic/kibana/issues/27410 + * TODO: Once the Discover query has been update, we should change the below to use the new field + */ + const { intervalOpenSearchValue, intervalOpenSearchUnit, interval } = chartData.ordered; + const xInterval = interval.asMilliseconds(); + + const xValues = chartData.xAxisOrderedValues; + const lastXValue = xValues[xValues.length - 1]; + + const domain = chartData.ordered; + const domainStart = domain.min.valueOf(); + const domainEnd = domain.max.valueOf(); + + const domainMin = data[0]?.x > domainStart ? domainStart : data[0]?.x; + const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue; + + const xDomain = { + min: domainMin, + max: domainMax, + minInterval: findMinInterval( + xValues, + intervalOpenSearchValue, + intervalOpenSearchUnit, + timeZone + ), + }; + + // Domain end of 'now' will be milliseconds behind current time, so we extend time by 1 minute and check if + // the annotation is within this range; if so, the line annotation uses the domainEnd as its value + const now = moment(); + const isAnnotationAtEdge = moment(domainEnd).add(60000).isAfter(now) && now.isAfter(domainEnd); + const lineAnnotationValue = isAnnotationAtEdge ? domainEnd : now; + + const lineAnnotationData = [ + { + dataValue: lineAnnotationValue, + }, + ]; + const isDarkMode = uiSettings.get('theme:darkMode'); + + const lineAnnotationStyle = { + line: { + strokeWidth: 2, + stroke: euiThemeVars.euiColorDanger, + opacity: 0.7, + }, + }; + + const rectAnnotations = []; + if (domainStart !== domainMin) { + rectAnnotations.push({ + coordinates: { + x1: domainStart, + }, + }); + } + if (domainEnd !== domainMax) { + rectAnnotations.push({ + coordinates: { + x0: domainEnd, + }, + }); + } + + const rectAnnotationStyle = { + stroke: isDarkMode ? euiThemeVars.euiColorLightShade : euiThemeVars.euiColorDarkShade, + strokeWidth: 0, + opacity: isDarkMode ? 0.6 : 0.2, + fill: isDarkMode ? euiThemeVars.euiColorLightShade : euiThemeVars.euiColorDarkShade, + }; + + const tooltipProps = { + headerFormatter: this.renderBarTooltip(xInterval, domainStart, domainEnd), + type: TooltipType.VerticalCursor, + }; + + return ( + + + + + + + + + ); + } +} diff --git a/src/plugins/discover/public/application/components/chart/histogram/type.ts b/src/plugins/discover/public/application/components/chart/histogram/type.ts new file mode 100644 index 000000000000..64c5a1124731 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/histogram/type.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface Chart { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx rename to src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.test.tsx diff --git a/src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.tsx new file mode 100644 index 000000000000..fede3e04ecd4 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/hits_counter/hits_counter.tsx @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +import { formatNumWithCommas } from '../../../helpers'; + +export interface HitsCounterProps { + /** + * the number of query hits + */ + hits: number; + /** + * displays the reset button + */ + showResetButton: boolean; + /** + * resets the query + */ + onResetQuery: () => void; +} + +export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) { + return ( + + + + + {formatNumWithCommas(hits)}{' '} + + + + {showResetButton && ( + + + + + + )} + + + ); +} diff --git a/src/plugins/discover/public/application/components/hits_counter/index.ts b/src/plugins/discover/public/application/components/chart/hits_counter/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/hits_counter/index.ts rename to src/plugins/discover/public/application/components/chart/hits_counter/index.ts diff --git a/src/plugins/discover/public/application/components/chart/timechart_header/index.ts b/src/plugins/discover/public/application/components/chart/timechart_header/index.ts new file mode 100644 index 000000000000..34763a05f8e0 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/timechart_header/index.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './timechart_header'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx rename to src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.test.tsx diff --git a/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx new file mode 100644 index 000000000000..a79876ccb455 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx @@ -0,0 +1,189 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiText, + EuiSelect, + EuiIconTip, +} from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +import moment from 'moment'; + +export interface TimechartHeaderBucketInterval { + scaled?: boolean; + description?: string; + scale?: number; +} + +export interface TimechartHeaderProps { + /** + * Format of date to be displayed + */ + dateFormat?: string; + /** + * Interval for the buckets of the recent request + */ + bucketInterval?: { + scaled?: boolean; + description?: string; + scale?: number; + }; + /** + * Range of dates to be displayed + */ + timeRange?: { + from: string; + to: string; + }; + /** + * Interval Options + */ + options: Array<{ display: string; val: string }>; + /** + * changes the interval + */ + onChangeInterval: (interval: string) => void; + /** + * selected interval + */ + stateInterval: string; +} + +export function TimechartHeader({ + bucketInterval, + dateFormat, + timeRange, + options, + onChangeInterval, + stateInterval, +}: TimechartHeaderProps) { + const [interval, setInterval] = useState(stateInterval); + const toMoment = useCallback( + (datetime: string) => { + if (!datetime) { + return ''; + } + if (!dateFormat) { + return datetime; + } + return moment(datetime).format(dateFormat); + }, + [dateFormat] + ); + + useEffect(() => { + setInterval(stateInterval); + }, [stateInterval]); + + const handleIntervalChange = (e: React.ChangeEvent) => { + setInterval(e.target.value); + onChangeInterval(e.target.value); + }; + + if (!timeRange || !bucketInterval) { + return null; + } + + return ( + + + + + + {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ + interval !== 'auto' + ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { + defaultMessage: 'per', + }) + : '' + }`} + + + + + val !== 'custom') + .map(({ display, val }) => { + return { + text: display, + value: val, + label: display, + }; + })} + value={interval} + onChange={handleIntervalChange} + append={ + bucketInterval.scaled ? ( + 1 + ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { + defaultMessage: 'buckets that are too large', + }) + : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { + defaultMessage: 'too many buckets', + }), + bucketIntervalDescription: bucketInterval.description, + }, + })} + color="warning" + size="s" + type="alert" + /> + ) : undefined + } + /> + + + + ); +} diff --git a/src/plugins/discover/public/application/components/chart/utils/create_histogram_configs.ts b/src/plugins/discover/public/application/components/chart/utils/create_histogram_configs.ts new file mode 100644 index 000000000000..8e659e73f9d7 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/create_histogram_configs.ts @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataPublicPluginStart, IndexPattern } from '../../../../../../data/public'; + +export function createHistogramConfigs( + indexPattern: IndexPattern, + histogramInterval: string, + data: DataPublicPluginStart +) { + const visStateAggs = [ + { + type: 'count', + schema: 'metric', + }, + { + type: 'date_histogram', + schema: 'segment', + params: { + field: indexPattern.timeFieldName!, + interval: histogramInterval, + timeRange: data.query.timefilter.timefilter.getTime(), + }, + }, + ]; + return data.search.aggs.createAggConfigs(indexPattern, visStateAggs); +} diff --git a/src/plugins/discover/public/application/components/chart/utils/get_dimension.test.ts b/src/plugins/discover/public/application/components/chart/utils/get_dimension.test.ts new file mode 100644 index 000000000000..a4f1c889a9ba --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/get_dimension.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getDimensions } from './get_dimensions'; +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import { search } from '../../../../../../data/public'; +import { dataPluginMock } from '../../../../../../data/public/mocks'; +import { + calculateBounds, + IBucketDateHistogramAggConfig, + IAggConfigs, +} from '../../../../../../data/common'; + +describe('getDimensions', () => { + it('should return dimensions when buckets and bounds are defined', () => { + const dataMock = dataPluginMock.createStartContract(); + dataMock.query.timefilter.timefilter.getTime = () => { + return { from: '2021-01-01T00:00:00.000Z', to: '2021-01-31T00:00:00.000Z' }; + }; + dataMock.query.timefilter.timefilter.calculateBounds = (timeRange) => { + return calculateBounds(timeRange); + }; + + const metric = { + toSerializedFieldFormat: jest.fn(() => 'metric-format'), + makeLabel: jest.fn(() => 'metric-label'), + }; + const agg = { + params: { timeRange: null }, + buckets: { + getInterval: jest.fn(() => ({ opensearchUnit: 'day', opensearchValue: 1 })), + getScaledDateFormat: jest.fn(() => 'scaled-date-format'), + getBounds: jest.fn(() => 'bounds'), + }, + makeLabel: jest.fn(() => 'agg-label'), + toSerializedFieldFormat: jest.fn(() => 'agg-format'), + }; + const aggs: IAggConfigs = { + aggs: [metric, agg], + }; + + // Mocking external dependencies + dateMath.parse = jest.fn((date, options) => moment(date)); + search.aggs.isDateHistogramBucketAggConfig = jest.fn( + (bucketAgg: any): bucketAgg is IBucketDateHistogramAggConfig => true + ); + + const result = getDimensions(aggs, dataMock); + + expect(result).toEqual({ + x: { + accessor: 0, + label: 'agg-label', + format: 'agg-format', + params: { + date: true, + interval: moment.duration(1, 'day'), + intervalOpenSearchValue: 1, + intervalOpenSearchUnit: 'day', + format: 'scaled-date-format', + bounds: 'bounds', + }, + }, + y: { + accessor: 1, + format: 'metric-format', + label: 'metric-label', + }, + }); + }); +}); diff --git a/src/plugins/discover/public/application/components/chart/utils/get_dimensions.ts b/src/plugins/discover/public/application/components/chart/utils/get_dimensions.ts new file mode 100644 index 000000000000..a1292e8aefa6 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/get_dimensions.ts @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import dateMath from '@elastic/datemath'; +import { IAggConfigs } from '../../../../../../data/common'; +import { search } from '../../../../../../data/public'; + +export function getDimensions(aggs: IAggConfigs, data: any) { + const [metric, agg] = aggs.aggs; + const { from, to } = data.query.timefilter.timefilter.getTime(); + agg.params.timeRange = { + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), + }; + const bounds = agg.params.timeRange + ? data.query.timefilter.timefilter.calculateBounds(agg.params.timeRange) + : null; + const buckets = search.aggs.isDateHistogramBucketAggConfig(agg) ? agg.buckets : undefined; + + if (!buckets || !bounds) { + return; + } + + const { opensearchUnit, opensearchValue } = buckets.getInterval(); + return { + x: { + accessor: 0, + label: agg.makeLabel(), + format: agg.toSerializedFieldFormat(), + params: { + date: true, + interval: moment.duration(opensearchValue, opensearchUnit), + intervalOpenSearchValue: opensearchValue, + intervalOpenSearchUnit: opensearchUnit, + format: buckets.getScaledDateFormat(), + bounds: buckets.getBounds(), + }, + }, + y: { + accessor: 1, + format: metric.toSerializedFieldFormat(), + label: metric.makeLabel(), + }, + }; +} diff --git a/src/plugins/discover/public/application/components/chart/utils/index.ts b/src/plugins/discover/public/application/components/chart/utils/index.ts new file mode 100644 index 000000000000..36aa4015782e --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './create_histogram_configs'; +export * from './point_series'; +export * from './get_dimensions'; diff --git a/src/plugins/discover/public/application/components/chart/utils/point_series.test.ts b/src/plugins/discover/public/application/components/chart/utils/point_series.test.ts new file mode 100644 index 000000000000..7348033e834f --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/point_series.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { buildPointSeriesData, Dimensions, Table } from './point_series'; +import moment from 'moment'; + +describe('buildPointSeriesData', () => { + it('should build the chart data from the table and dimensions', () => { + const table: Table = { + columns: [ + { id: 'x', name: 'X Axis' }, + { id: 'y', name: 'Y Axis' }, + ], + rows: [ + { x: 10, y: 100 }, + { x: 20, y: 200 }, + { x: 10, y: 'NaN' }, // This row should be ignored + ], + }; + + const dimensions: Dimensions = { + x: { + accessor: 0, + format: { id: 'number', params: { pattern: 'number' } }, + params: { + date: true, + interval: moment.duration(1, 'hour'), + intervalOpenSearchValue: 1, + intervalOpenSearchUnit: 'h', + format: 'number', + bounds: { + min: moment('2023-01-01'), + max: moment('2023-01-02'), + }, + }, + }, + y: { + accessor: 1, + format: { id: 'number', params: { pattern: 'number' } }, + }, + }; + + const result = buildPointSeriesData(table, dimensions); + + expect(result.xAxisOrderedValues).toEqual([10, 20]); + expect(result.xAxisFormat).toEqual(dimensions.x.format); + expect(result.xAxisLabel).toEqual('X Axis'); + expect(result.ordered.date).toBe(true); + expect(result.ordered.interval.asHours()).toBe(1); + expect(result.ordered.intervalOpenSearchUnit).toBe('h'); + expect(result.ordered.intervalOpenSearchValue).toBe(1); + expect(result.ordered.min.format()).toBe('2023-01-01T00:00:00+00:00'); + expect(result.ordered.max.format()).toBe('2023-01-02T00:00:00+00:00'); + expect(result.yAxisLabel).toEqual('Y Axis'); + expect(result.values).toEqual([ + { x: 10, y: 100 }, + { x: 20, y: 200 }, + ]); + }); +}); diff --git a/src/plugins/discover/public/application/components/chart/utils/point_series.ts b/src/plugins/discover/public/application/components/chart/utils/point_series.ts new file mode 100644 index 000000000000..4c3e3e9ada53 --- /dev/null +++ b/src/plugins/discover/public/application/components/chart/utils/point_series.ts @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { uniq } from 'lodash'; +import { Duration, Moment } from 'moment'; +import { Unit } from '@elastic/datemath'; + +import { SerializedFieldFormat } from '../../../../../../expressions/common/types'; + +export interface Column { + id: string; + name: string; +} + +export interface Row { + [key: string]: number | 'NaN'; +} + +export interface Table { + columns: Column[]; + rows: Row[]; +} + +interface HistogramParams { + date: true; + interval: Duration; + intervalOpenSearchValue: number; + intervalOpenSearchUnit: Unit; + format: string; + bounds: { + min: Moment; + max: Moment; + }; +} +export interface Dimension { + accessor: 0 | 1; + format: SerializedFieldFormat<{ pattern: string }>; +} + +export interface Dimensions { + x: Dimension & { params: HistogramParams }; + y: Dimension; +} + +interface Ordered { + date: true; + interval: Duration; + intervalOpenSearchUnit: string; + intervalOpenSearchValue: number; + min: Moment; + max: Moment; +} +export interface Chart { + values: Array<{ + x: number; + y: number; + }>; + xAxisOrderedValues: number[]; + xAxisFormat: Dimension['format']; + xAxisLabel: Column['name']; + yAxisLabel?: Column['name']; + ordered: Ordered; +} + +export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { + const { x, y } = dimensions; + const xAccessor = table.columns[x.accessor].id; + const yAccessor = table.columns[y.accessor].id; + const chart = {} as Chart; + + chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); + chart.xAxisFormat = x.format; + chart.xAxisLabel = table.columns[x.accessor].name; + + const { intervalOpenSearchUnit, intervalOpenSearchValue, interval, bounds } = x.params; + chart.ordered = { + date: true, + interval, + intervalOpenSearchUnit, + intervalOpenSearchValue, + min: bounds.min, + max: bounds.max, + }; + + chart.yAxisLabel = table.columns[y.accessor].name; + + chart.values = table.rows + .filter((row) => row && row[yAccessor] !== 'NaN') + .map((row) => ({ + x: row[xAccessor] as number, + y: row[yAccessor] as number, + })); + + return chart; +}; diff --git a/src/plugins/discover/public/application/components/data_grid/constants.ts b/src/plugins/discover/public/application/components/data_grid/constants.ts new file mode 100644 index 000000000000..c11f205aff1f --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const toolbarVisibility = { + showColumnSelector: { + allowHide: false, + allowReorder: true, + }, + showStyleSelector: false, +}; diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx new file mode 100644 index 000000000000..d304034d3760 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx @@ -0,0 +1,165 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useMemo, useCallback } from 'react'; +import { EuiDataGrid, EuiDataGridSorting, EuiPanel } from '@elastic/eui'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { fetchTableDataCell } from './data_grid_table_cell_value'; +import { buildDataGridColumns, computeVisibleColumns } from './data_grid_table_columns'; +import { DocViewInspectButton } from './data_grid_table_docview_inspect_button'; +import { DataGridFlyout } from './data_grid_table_flyout'; +import { DiscoverGridContextProvider } from './data_grid_table_context'; +import { toolbarVisibility } from './constants'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { DiscoverServices } from '../../../build_services'; +import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; +import { usePagination } from '../utils/use_pagination'; +import { SortOrder } from '../../../saved_searches/types'; + +export interface DataGridTableProps { + columns: string[]; + indexPattern: IndexPattern; + onAddColumn: (column: string) => void; + onFilter: DocViewFilterFn; + onRemoveColumn: (column: string) => void; + onSort: (sort: SortOrder[]) => void; + rows: OpenSearchSearchHit[]; + onSetColumns: (columns: string[]) => void; + sort: SortOrder[]; + displayTimeColumn: boolean; + services: DiscoverServices; + isToolbarVisible?: boolean; +} + +export const DataGridTable = ({ + columns, + indexPattern, + onAddColumn, + onFilter, + onRemoveColumn, + onSetColumns, + onSort, + sort, + rows, + displayTimeColumn, + isToolbarVisible = true, +}: DataGridTableProps) => { + const [inspectedHit, setInspectedHit] = useState(); + const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]); + const pagination = usePagination(rowCount); + + const sortingColumns = useMemo(() => sort.map(([id, direction]) => ({ id, direction })), [sort]); + const rowHeightsOptions = useMemo( + () => ({ + defaultHeight: { + lineCount: columns.includes('_source') ? 3 : 1, + }, + }), + [columns] + ); + + const onColumnSort = useCallback( + (cols: EuiDataGridSorting['columns']) => { + onSort(cols.map(({ id, direction }) => [id, direction])); + }, + [onSort] + ); + + const renderCellValue = useMemo(() => fetchTableDataCell(indexPattern, rows), [ + indexPattern, + rows, + ]); + + const dataGridTableColumns = useMemo( + () => buildDataGridColumns(columns, indexPattern, displayTimeColumn), + [columns, indexPattern, displayTimeColumn] + ); + + const dataGridTableColumnsVisibility = useMemo( + () => ({ + visibleColumns: computeVisibleColumns(columns, indexPattern, displayTimeColumn) as string[], + setVisibleColumns: (cols: string[]) => { + onSetColumns(cols); + }, + }), + [columns, indexPattern, displayTimeColumn, onSetColumns] + ); + + const sorting: EuiDataGridSorting = useMemo( + () => ({ columns: sortingColumns, onSort: onColumnSort }), + [sortingColumns, onColumnSort] + ); + + const leadingControlColumns = useMemo(() => { + return [ + { + id: 'inspectCollapseColumn', + headerCellRender: () => null, + rowCellRender: DocViewInspectButton, + width: 40, + }, + ]; + }, []); + + const table = useMemo( + () => ( + + ), + [ + dataGridTableColumns, + dataGridTableColumnsVisibility, + leadingControlColumns, + pagination, + renderCellValue, + rowCount, + sorting, + isToolbarVisible, + rowHeightsOptions, + ] + ); + + return ( + + <> + + + {table} + + + {inspectedHit && ( + setInspectedHit(undefined)} + /> + )} + + + ); +}; diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx new file mode 100644 index 000000000000..3d81ba1afb33 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { IndexPatternField } from '../../../../../data/common'; +import { useDataGridContext } from './data_grid_table_context'; + +export function getCellActions(field: IndexPatternField) { + const cellActions = field.filterable + ? [ + ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => { + const { indexPattern, rows, onFilter } = useDataGridContext(); + + const filterForValueText = i18n.translate('discover.filterForValue', { + defaultMessage: 'Filter for value', + }); + const filterForValueLabel = i18n.translate('discover.filterForValueLabel', { + defaultMessage: 'Filter for value: {value}', + values: { value: columnId }, + }); + + return ( + { + const row = rows[rowIndex]; + const flattened = indexPattern.flattenHit(row); + + if (flattened) { + onFilter(columnId, flattened[columnId], '+'); + } + }} + iconType="plusInCircle" + aria-label={filterForValueLabel} + data-test-subj="filterForValue" + > + {filterForValueText} + + ); + }, + ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => { + const { indexPattern, rows, onFilter } = useDataGridContext(); + + const filterOutValueText = i18n.translate('discover.filterOutValue', { + defaultMessage: 'Filter out value', + }); + const filterOutValueLabel = i18n.translate('discover.filterOutValueLabel', { + defaultMessage: 'Filter out value: {value}', + values: { value: columnId }, + }); + + return ( + { + const row = rows[rowIndex]; + const flattened = indexPattern.flattenHit(row); + + if (flattened) { + onFilter(columnId, flattened[columnId], '-'); + } + }} + iconType="minusInCircle" + aria-label={filterOutValueLabel} + data-test-subj="filterOutValue" + > + {filterOutValueText} + + ); + }, + ] + : undefined; + return cellActions; +} diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.test.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.test.tsx new file mode 100644 index 000000000000..e9c867a53db7 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.test.tsx @@ -0,0 +1,187 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { + indexPatternMock, + getMockedIndexPatternWithCustomizedFields, +} from '../../../__mock__/index_pattern_mock'; +import { fetchTableDataCell } from './data_grid_table_cell_value'; + +const fieldsData = [ + { + name: 'name', + scripted: false, + filterable: true, + aggregatable: false, + searchable: true, + sortable: false, + }, + { + name: 'currency', + type: 'string', + scripted: false, + filterable: true, + aggregatable: true, + searchable: true, + sortable: true, + }, + { + name: 'order_date', + type: 'date', + scripted: false, + filterable: true, + aggregatable: true, + searchable: true, + sortable: true, + }, +]; + +const dataRowsMock = [ + { + _id: '1', + _index: 'test_index', + _score: 0, + _source: { + name: 'Eddie', + currency: 'EUR', + order_date: '2023-08-07T09:28:48+00:00', + }, + fields: { + order_date: ['2023-08-07T09:28:48.000Z'], + }, + _version: 1, + _type: '_doc', + }, +]; + +const customizedIndexPatternMock = getMockedIndexPatternWithCustomizedFields( + fieldsData +) as IndexPattern; + +describe('Testing fetchTableDataCell function', () => { + it('should display empty span if no data', () => { + const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock); + const comp = shallow( + + ); + + expect(comp).toMatchInlineSnapshot(` + + - + + `); + }); + + it('should display empty span if field is not defined in index pattern', () => { + const DataGridTableCellValue = fetchTableDataCell(indexPatternMock, dataRowsMock); + const comp = shallow( + + ); + + expect(comp).toMatchInlineSnapshot(` + + - + + `); + }); + + it('should display JSON string representation of the data if columnId is _source and isDetails is false', () => { + const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const comp = shallow( + + ); + + expect(comp).toMatchInlineSnapshot(` + + { + "name": "Eddie", + "currency": "EUR", + "order_date": "2023-08-07T09:28:48+00:00" + } + + `); + }); + + it('should display EuiDescriptionList if columnId is _source and isDetails is false', () => { + const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const comp = shallow( + + ); + + expect(comp).toMatchInlineSnapshot(` + + + order_date + + + + `); + }); + + it('should correctly display data if columnId is in index pattern and is not _source', () => { + const DataGridTableCellValue = fetchTableDataCell(customizedIndexPatternMock, dataRowsMock); + const comp = shallow( + + ); + + expect(comp).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.tsx new file mode 100644 index 000000000000..2e39dde3ba06 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_value.tsx @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Fragment } from 'react'; +import dompurify from 'dompurify'; + +import { + EuiDataGridCellValueElementProps, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; + +function fetchSourceTypeDataCell( + idxPattern: IndexPattern, + row: Record, + columnId: string, + isDetails: boolean +) { + if (isDetails) { + return {JSON.stringify(row[columnId], null, 2)}; + } + const formattedRow = idxPattern.formatHit(row); + + return ( + + {Object.keys(formattedRow).map((key) => ( + + {key} + + + ))} + + ); +} + +export const fetchTableDataCell = ( + idxPattern: IndexPattern, + dataRows: OpenSearchSearchHit[] | undefined +) => ({ rowIndex, columnId, isDetails }: EuiDataGridCellValueElementProps) => { + const singleRow = dataRows ? (dataRows[rowIndex] as Record) : undefined; + const flattenedRows = dataRows ? dataRows.map((hit) => idxPattern.flattenHit(hit)) : []; + const flattenedRow = flattenedRows + ? (flattenedRows[rowIndex] as Record) + : undefined; + const fieldInfo = idxPattern.fields.getByName(columnId); + + if (typeof singleRow === 'undefined' || typeof flattenedRow === 'undefined') { + return -; + } + + if (!fieldInfo?.type && flattenedRow && typeof flattenedRow[columnId] === 'object') { + if (isDetails) { + return {JSON.stringify(flattenedRow[columnId], null, 2)}; + } + + return {JSON.stringify(flattenedRow[columnId])}; + } + + if (fieldInfo?.type === '_source') { + return fetchSourceTypeDataCell(idxPattern, singleRow, columnId, isDetails); + } + + const formattedValue = idxPattern.formatField(singleRow, columnId); + if (typeof formattedValue === 'undefined') { + return -; + } else { + const sanitizedCellValue = dompurify.sanitize(idxPattern.formatField(singleRow, columnId)); + return ( + // eslint-disable-next-line react/no-danger + + ); + } +}; diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.test.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.test.tsx new file mode 100644 index 000000000000..9a53bff57e16 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.test.tsx @@ -0,0 +1,249 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { + getMockedIndexPatternWithCustomizedFields, + getMockedIndexPatternWithTimeField, +} from '../../../__mock__/index_pattern_mock'; +import { buildDataGridColumns, computeVisibleColumns } from './data_grid_table_columns'; + +const fieldsData = [ + { + name: 'name', + scripted: false, + filterable: true, + aggregatable: false, + searchable: true, + sortable: false, + }, + { + name: 'currency', + type: 'string', + scripted: false, + filterable: true, + aggregatable: true, + searchable: true, + sortable: true, + }, + { + name: 'order_date', + type: 'date', + scripted: false, + filterable: true, + aggregatable: true, + searchable: true, + sortable: true, + }, +]; + +const customizedIndexPatternMock = getMockedIndexPatternWithCustomizedFields( + fieldsData +) as IndexPattern; +const customizedIndexPatternMockWithTimeField = getMockedIndexPatternWithTimeField( + fieldsData, + 'order_date' +) as IndexPattern; + +describe('Testing buildDataGridColumns function ', () => { + it('should return correct columns without time column when displayTimeColumn is false', () => { + const columns = buildDataGridColumns(['name', 'currency'], customizedIndexPatternMock, false); + expect(columns).toHaveLength(2); + expect(columns[0].id).toEqual('name'); + expect(columns[1].id).toEqual('currency'); + expect(columns).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "name", + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "currency", + "isSortable": undefined, + "schema": undefined, + }, + ] + `); + }); + + it('should add time and source columns correctly when displayTimeColumn is true', () => { + const columns = buildDataGridColumns( + ['name', 'currency', '_source'], + customizedIndexPatternMockWithTimeField, + true + ); + expect(columns).toHaveLength(4); + expect(columns[0].id).toEqual('order_date'); + expect(columns[0].display).toEqual('Time (order_date)'); + expect(columns[3].id).toEqual('_source'); + expect(columns[3].display).toEqual('Source'); + expect(columns).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": "Time (order_date)", + "id": "order_date", + "initialWidth": 200, + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "name", + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "currency", + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": "Source", + "id": "_source", + "isSortable": undefined, + "schema": undefined, + }, + ] + `); + }); + + it('should set display for time column correctly when time field is already included', () => { + const columns = buildDataGridColumns( + ['name', 'currency', 'order_date'], + customizedIndexPatternMockWithTimeField, + true + ); + expect(columns).toHaveLength(3); + expect(columns[2].id).toEqual('order_date'); + expect(columns[2].display).toEqual('Time (order_date)'); + expect(columns).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "name", + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": undefined, + "id": "currency", + "isSortable": undefined, + "schema": undefined, + }, + Object { + "actions": Object { + "showHide": true, + "showMoveLeft": false, + "showMoveRight": false, + }, + "cellActions": undefined, + "display": "Time (order_date)", + "id": "order_date", + "initialWidth": 200, + "isSortable": undefined, + "schema": undefined, + }, + ] + `); + }); +}); + +describe('Testing computeVisibleColumns function ', () => { + it('should include time column when displayTimeColumn is true and time field is missing', () => { + const visibleColumns = computeVisibleColumns( + ['name', 'currency'], + customizedIndexPatternMock, + true + ); + expect(visibleColumns).toMatchInlineSnapshot(` + Array [ + "order_date", + "name", + "currency", + ] + `); + }); + + it('should not add duplicate time column when displayTimeColumn is true and time field is included', () => { + const visibleColumns = computeVisibleColumns( + ['name', 'currency', 'order_date'], + customizedIndexPatternMock, + true + ); + expect(visibleColumns).toMatchInlineSnapshot(` + Array [ + "name", + "currency", + "order_date", + ] + `); + }); + + it('should not add time column when displayTimeColumn is false', () => { + const visibleColumns = computeVisibleColumns( + ['name', 'currency'], + customizedIndexPatternMock, + false + ); + expect(visibleColumns).toMatchInlineSnapshot(` + Array [ + "name", + "currency", + ] + `); + }); +}); diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx new file mode 100644 index 000000000000..f38debbd7be3 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiDataGridColumn } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { getCellActions } from './data_grid_table_cell_actions'; + +export function buildDataGridColumns( + columnNames: string[], + idxPattern: IndexPattern, + displayTimeColumn: boolean +) { + const timeFieldName = idxPattern.timeFieldName; + let columnsToUse = columnNames; + + if (displayTimeColumn && idxPattern.timeFieldName && !columnNames.includes(timeFieldName)) { + columnsToUse = [idxPattern.timeFieldName, ...columnNames]; + } + + return columnsToUse.map((colName) => generateDataGridTableColumn(colName, idxPattern)); +} + +export function generateDataGridTableColumn(colName: string, idxPattern: IndexPattern) { + const timeLabel = i18n.translate('discover.timeLabel', { + defaultMessage: 'Time', + }); + const idxPatternField = idxPattern.getFieldByName(colName); + const dataGridCol: EuiDataGridColumn = { + id: colName, + schema: idxPatternField?.type, + isSortable: idxPatternField?.sortable, + display: idxPatternField?.displayName, + actions: { + showHide: true, + showMoveLeft: false, + showMoveRight: false, + }, + cellActions: idxPatternField ? getCellActions(idxPatternField) : [], + }; + + if (dataGridCol.id === idxPattern.timeFieldName) { + dataGridCol.display = `${timeLabel} (${idxPattern.timeFieldName})`; + dataGridCol.initialWidth = 200; + } + if (dataGridCol.id === '_source') { + dataGridCol.display = i18n.translate('discover.sourceLabel', { + defaultMessage: 'Source', + }); + } + return dataGridCol; +} + +export function computeVisibleColumns( + columnNames: string[], + idxPattern: IndexPattern, + displayTimeColumn: boolean +) { + const timeFieldName = idxPattern.timeFieldName; + let visibleColumnNames = columnNames; + + if (displayTimeColumn && !columnNames.includes(timeFieldName)) { + visibleColumnNames = [timeFieldName, ...columnNames]; + } + + return visibleColumnNames; +} diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx new file mode 100644 index 000000000000..886b6fa60f8d --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types'; + +export interface DataGridContextProps { + inspectedHit?: OpenSearchSearchHit; + onFilter: DocViewFilterFn; + setInspectedHit: (hit?: OpenSearchSearchHit) => void; + rows: OpenSearchSearchHit[]; + indexPattern: IndexPattern; +} + +export const DataGridContext = React.createContext( + {} as DataGridContextProps +); + +export const DiscoverGridContextProvider = DataGridContext.Provider; +export const useDataGridContext = () => React.useContext(DataGridContext); diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_inspect_button.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_inspect_button.tsx new file mode 100644 index 000000000000..df5e691240e3 --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_inspect_button.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiToolTip, EuiButtonIcon, EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { useDataGridContext } from './data_grid_table_context'; + +export const DocViewInspectButton = ({ rowIndex }: EuiDataGridCellValueElementProps) => { + const { inspectedHit, setInspectedHit, rows } = useDataGridContext(); + const currentInspected = rows[rowIndex]; + const isCurrentInspected = currentInspected === inspectedHit; + const inspectHintMsg = i18n.translate('discover.docViews.table.inspectAriaLabel', { + defaultMessage: 'Inspect document details', + }); + + return ( + + setInspectedHit(isCurrentInspected ? undefined : currentInspected)} + iconType={isCurrentInspected ? 'minimize' : 'inspect'} + aria-label={inspectHintMsg} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx new file mode 100644 index 000000000000..957679a2faef --- /dev/null +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, +} from '@elastic/eui'; +import { DocViewer } from '../doc_viewer/doc_viewer'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { DocViewerLinks } from '../doc_viewer_links/doc_viewer_links'; + +interface Props { + columns: string[]; + hit: any; + indexPattern: IndexPattern; + onAddColumn: (column: string) => void; + onClose: () => void; + onFilter: DocViewFilterFn; + onRemoveColumn: (column: string) => void; +} + +export function DataGridFlyout({ + hit, + columns, + indexPattern, + onAddColumn, + onClose, + onFilter, + onRemoveColumn, +}: Props) { + // TODO: replace EuiLink with doc_view_links registry + // TODO: Also move the flyout higher in the react tree to prevent redrawing the table component and slowing down page performance + return ( + + + +

Document Details

+
+
+ + + + + + + { + onRemoveColumn(columnName); + onClose(); + }} + onAddColumn={(columnName: string) => { + onAddColumn(columnName); + onClose(); + }} + filter={(mapping, value, mode) => { + onFilter(mapping, value, mode); + onClose(); + }} + /> + + + +
+ ); +} diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx index edc7f40c5e43..17736de5de2a 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_tab.tsx @@ -37,7 +37,6 @@ interface Props { } /** * Responsible for rendering a tab provided by a render function. - * So any other framework can be used (E.g. legacy Angular 3rd party plugin code) * The provided `render` function is called with a reference to the * component's `HTMLDivElement` as 1st arg and `renderProps` as 2nd arg */ diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap index 95fb0c377180..2a3b5a3aa998 100644 --- a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap +++ b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap @@ -3,14 +3,17 @@ exports[`Dont Render if generateCb.hide 1`] = ` `; exports[`Render with 2 different links 1`] = ` with 2 different links 1`] = ` /> + {listItems.map((item, index) => ( - + ))} diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts b/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts new file mode 100644 index 000000000000..47bf0dbdcf90 --- /dev/null +++ b/src/plugins/discover/public/application/components/help_menu/help_menu_util.ts @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { ChromeStart } from 'opensearch-dashboards/public'; +import { getServices } from '../../../opensearch_dashboards_services'; + +const { docLinks } = getServices(); + +export function addHelpMenuToAppChrome(chrome: ChromeStart) { + chrome.setHelpExtension({ + appName: i18n.translate('discover.helpMenu.appName', { + defaultMessage: 'Discover', + }), + links: [ + { + linkType: 'documentation', + href: `${docLinks.links.opensearchDashboards.introduction}`, + }, + ], + }); +} diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss new file mode 100644 index 000000000000..051ab642c1cf --- /dev/null +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.scss @@ -0,0 +1,4 @@ +.discoverNoResults { + display: flex; + align-items: center; +} diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx index 697c7a136d60..dc12ba4581fc 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx @@ -28,20 +28,24 @@ * under the License. */ +import './loading_spinner.scss'; import React from 'react'; -import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiPanel, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; export function LoadingSpinner() { return ( - <> - -

- -

-
- - - + + } + title={ + +

+ +

+
+ } + /> +
); } diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx new file mode 100644 index 000000000000..ad7bab0e81e9 --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -0,0 +1,212 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Fragment } from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; + +import { + EuiCallOut, + EuiCode, + EuiDescriptionList, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { getServices } from '../../../opensearch_dashboards_services'; + +interface Props { + timeFieldName?: string; + queryLanguage?: string; +} + +export const DiscoverNoResults = ({ timeFieldName, queryLanguage }: Props) => { + let timeFieldMessage; + + if (timeFieldName) { + timeFieldMessage = ( + + + + +

+ +

+ +

+ +

+
+
+ ); + } + + let luceneQueryMessage; + + if (queryLanguage === 'lucene') { + const searchExamples = [ + { + description: 200, + title: ( + + + + + + ), + }, + { + description: status:200, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499], + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND extension:PHP, + title: ( + + + + + + ), + }, + { + description: status:[400 TO 499] AND (extension:php OR extension:html), + title: ( + + + + + + ), + }, + ]; + + luceneQueryMessage = ( + + + + +

+ +

+ +

+ + + + ), + }} + /> +

+
+ + + + + + +
+ ); + } + + return ( + + + + } + color="warning" + iconType="help" + data-test-subj="discoverNoResults" + /> + {timeFieldMessage} + {luceneQueryMessage} + + + ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 1b384a4b5550..29d78448f087 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -30,10 +30,8 @@ import React from 'react'; // @ts-ignore -import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { render, screen, fireEvent } from 'test_utils/testing_lib_helpers'; import { DiscoverField } from './discover_field'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; @@ -63,7 +61,7 @@ jest.mock('../../../opensearch_dashboards_services', () => ({ }), })); -function getComponent({ +function getProps({ selected = false, showDetails = false, useShortDots = false, @@ -110,24 +108,33 @@ function getComponent({ selected, useShortDots, }; - const comp = mountWithIntl(); - return { comp, props }; + + return props; } describe('discover sidebar field', function () { - it('should allow selecting fields', function () { - const { comp, props } = getComponent({}); - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow selecting fields', async function () { + const props = getProps({}); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - const { comp, props } = getComponent({ selected: true }); - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow deselecting fields', async function () { + const props = getProps({ selected: true }); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); - it('should trigger getDetails', function () { - const { comp, props } = getComponent({ selected: true }); - findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + it('should trigger getDetails', async function () { + const props = getProps({ selected: true }); + render(); + + await fireEvent.click(screen.getByTestId('field-bytes-showDetails')); + expect(props.getDetails).toHaveBeenCalledWith(props.field); }); it('should not allow clicking on _source', function () { @@ -142,11 +149,12 @@ describe('discover sidebar field', function () { }, '_source' ); - const { comp, props } = getComponent({ + const props = getProps({ selected: true, field, }); - findTestSubject(comp, 'field-_source-showDetails').simulate('click'); - expect(props.getDetails).not.toHaveBeenCalled(); + render(); + + expect(screen.queryByTestId('field-_source-showDetails')).toBeNull(); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index e807267435eb..73dc40a262e0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -29,7 +29,15 @@ */ import React, { useState } from 'react'; -import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { + EuiPopover, + EuiPopoverTitle, + EuiButtonIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../opensearch_dashboards_react/public'; @@ -79,17 +87,17 @@ export interface DiscoverFieldProps { useShortDots?: boolean; } -export function DiscoverField({ - columns, +export const DiscoverField = ({ field, - indexPattern, + selected, onAddField, onRemoveField, + columns, + indexPattern, onAddFilter, getDetails, - selected, useShortDots, -}: DiscoverFieldProps) { +}: DiscoverFieldProps) => { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { defaultMessage: 'Add {field} to table', values: { field: field.name }, @@ -101,6 +109,11 @@ export function DiscoverField({ values: { field: field.name }, } ); + const infoLabelAria = i18n.translate('discover.fieldChooser.discoverField.infoButtonAriaLabel', { + defaultMessage: 'View {field} summary', + values: { field: field.name }, + }); + const isSourceField = field.name === '_source'; const [infoIsOpen, setOpen] = useState(false); @@ -112,10 +125,6 @@ export function DiscoverField({ } }; - function togglePopover() { - setOpen(!infoIsOpen); - } - function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows // the browser to efficiently word-wrap right after the dot @@ -123,22 +132,18 @@ export function DiscoverField({ return str ? str.replace(/\./g, '.\u200B') : ''; } - const dscFieldIcon = ( - - ); - const fieldName = ( {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)} ); let actionButton; - if (field.name !== '_source' && !selected) { + if (!isSourceField && !selected) { actionButton = ( ) => { if (ev.type === 'click') { ev.currentTarget.focus(); @@ -162,7 +166,7 @@ export function DiscoverField({ /> ); - } else if (field.name !== '_source' && selected) { + } else if (!isSourceField && selected) { actionButton = ( ) => { if (ev.type === 'click') { ev.currentTarget.focus(); @@ -189,57 +192,56 @@ export function DiscoverField({ ); } - if (field.type === '_source') { - return ( - - ); - } - return ( - { - togglePopover(); - }} - dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={dscFieldIcon} - fieldAction={actionButton} - fieldName={fieldName} - /> - } - isOpen={infoIsOpen} - closePopover={() => setOpen(false)} - anchorPosition="rightUp" - panelClassName="dscSidebarItem__fieldPopoverPanel" - > - - {' '} - {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} - - {infoIsOpen && ( - + + + + + {fieldName} + + {!isSourceField && ( + + setOpen(false)} + anchorPosition="rightUp" + button={ + setOpen((state) => !state)} + aria-label={infoLabelAria} + data-test-subj={`field-${field.name}-showDetails`} + /> + } + panelClassName="dscSidebarItem__fieldPopoverPanel" + > + + {' '} + {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} + + {infoIsOpen && ( + + )} + + )} - + {!isSourceField && {actionButton}} +
); -} +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 906c173ed07d..ce22761e75fa 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -40,7 +40,6 @@ import { } from './lib/visualize_trigger_utils'; import { Bucket, FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; -import './discover_field_details.scss'; interface DiscoverFieldDetailsProps { columns: string[]; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 9170adccc7e7..bcf72ae57326 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -31,8 +31,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; -import { DiscoverFieldSearch, Props } from './discover_field_search'; +import { findTestSubject } from 'test_utils/helpers'; +import { DiscoverFieldSearch, NUM_FILTERS, Props } from './discover_field_search'; import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; @@ -63,7 +63,7 @@ describe('DiscoverFieldSearch', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); let btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy(); + expect(btn.hasClass('euiFilterButton-hasActiveFilters')).toBeFalsy(); btn.simulate('click'); const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { @@ -72,7 +72,7 @@ describe('DiscoverFieldSearch', () => { }); component.update(); btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true); + expect(btn.hasClass('euiFilterButton-hasActiveFilters')).toBe(true); expect(onChange).toBeCalledWith('aggregatable', true); }); @@ -82,8 +82,8 @@ describe('DiscoverFieldSearch', () => { btn.simulate('click'); btn = findTestSubject(component, 'toggleFieldFilterButton'); const badge = btn.find('.euiNotificationBadge'); - // no active filters - expect(badge.text()).toEqual('0'); + // available filters + expect(badge.text()).toEqual(NUM_FILTERS.toString()); // change value of aggregatable select const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { @@ -114,10 +114,10 @@ describe('DiscoverFieldSearch', () => { const btn = findTestSubject(component, 'toggleFieldFilterButton'); btn.simulate('click'); const badge = btn.find('.euiNotificationBadge'); - expect(badge.text()).toEqual('0'); + expect(badge.text()).toEqual(NUM_FILTERS.toString()); const missingSwitch = findTestSubject(component, 'missingSwitch'); missingSwitch.simulate('change', { target: { value: false } }); - expect(badge.text()).toEqual('0'); + expect(badge.text()).toEqual(NUM_FILTERS.toString()); }); test('change in filters triggers onChange', () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 4a1390cb1955..8d90e0ae1099 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -31,11 +31,9 @@ import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; import { i18n } from '@osd/i18n'; import { - EuiFacetButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiPopover, EuiPopoverFooter, EuiPopoverTitle, @@ -46,9 +44,14 @@ import { EuiFormRow, EuiButtonGroup, EuiOutsideClickDetector, + EuiPanel, + EuiFilterButton, + EuiFilterGroup, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; +export const NUM_FILTERS = 3; + export interface State { searchable: string; aggregatable: string; @@ -106,12 +109,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { missing: true, }); - if (typeof value !== 'string') { - // at initial rendering value is undefined (angular related), this catches the warning - // should be removed once all is react - return null; - } - const filterBtnAriaLabel = isPopoverOpen ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { defaultMessage: 'Hide field filter settings', @@ -173,23 +170,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { handleValueChange('missing', missingValue); }; - const buttonContent = ( - } - isSelected={activeFiltersCount > 0} - quantity={activeFiltersCount} - onClick={handleFacetButtonClicked} - > - - - ); - const select = ( id: string, selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>, @@ -236,7 +216,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { legend={legend} options={toggleButtons(id)} idSelected={`${id}-${values[id]}`} - onChange={(optionId) => handleValueChange(id, optionId.replace(`${id}-`, ''))} + onChange={(optionId: string) => handleValueChange(id, optionId.replace(`${id}-`, ''))} buttonSize="compressed" isFullWidth data-test-subj={`${id}ButtonGroup`} @@ -245,7 +225,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { }; const selectionPanel = ( -
+ {buttonGroup('aggregatable', aggregatableLabel)} @@ -257,26 +237,25 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { {select('type', typeOptions, values.type)} -
+ ); return ( - - - + + + {}} isDisabled={!isPopoverOpen}> onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} value={value} /> - - -
- {}} isDisabled={!isPopoverOpen}> + + + + { setPopoverOpen(false); }} - button={buttonContent} + button={ + 0} + aria-label={filterBtnAriaLabel} + data-test-subj="toggleFieldFilterButton" + numFilters={NUM_FILTERS} + onClick={handleFacetButtonClicked} + numActiveFilters={activeFiltersCount} + isSelected={isPopoverOpen} + > + + + } > {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', { @@ -306,8 +302,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> - -
-
+ +
+
); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index 9c80e0afa600..63e8720f2c59 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,99 +1,3 @@ -.dscSidebar__container { - padding-left: 0 !important; - padding-right: 0 !important; - background-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; -} - -.dscIndexPattern__container { - display: flex; - align-items: center; - height: $euiSize * 3; - margin-top: -$euiSizeS; -} - -.dscIndexPattern__triggerButton { - @include euiTitle("xs"); - - line-height: $euiSizeXXL; -} - -.dscFieldList { - list-style: none; - margin-bottom: 0; -} - -.dscFieldListHeader { - padding: $euiSizeS $euiSizeS 0 $euiSizeS; - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); -} - -.dscFieldList--popular { - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); -} - -.dscFieldChooser { - padding-left: $euiSize; -} - -.dscFieldChooser__toggle { - color: $euiColorMediumShade; - margin-left: $euiSizeS !important; -} - -.dscSidebarItem { - &:hover, - &:focus-within, - &[class*="-isActive"] { - .dscSidebarItem__action { - opacity: 1; - } - } -} - -/** - * 1. Only visually hide the action, so that it's still accessible to screen readers. - * 2. When tabbed to, this element needs to be visible for keyboard accessibility. - */ -.dscSidebarItem__action { - opacity: 0; /* 1 */ - transition: none; - - &:focus { - opacity: 1; /* 2 */ - } - - font-size: $euiFontSizeXS; - padding: 2px 6px !important; - height: 22px !important; - min-width: auto !important; - - .euiButton__content { - padding: 0 4px; - } -} - -.dscFieldSearch { - padding: $euiSizeS; -} - -.dscFieldSearch__toggleButton { - width: calc(100% - #{$euiSizeS}); - color: $euiColorPrimary; - padding-left: $euiSizeXS; - margin-left: $euiSizeXS; -} - -.dscFieldSearch__filterWrapper { - flex-grow: 0; -} - -.dscFieldSearch__formWrapper { - padding: $euiSizeM; -} - -.dscFieldDetails { - color: $euiTextColor; - margin-bottom: $euiSizeS; +.dscSideBarFieldListHeader { + padding-left: $euiSizeS; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index dbc8c8962466..6fee8dde6b60 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -29,19 +29,15 @@ */ import _ from 'lodash'; -import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { render, screen, within, fireEvent } from '@testing-library/react'; import React from 'react'; import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; -import { SavedObject } from '../../../../../../core/types'; jest.mock('../../../opensearch_dashboards_services', () => ({ getServices: () => ({ @@ -74,7 +70,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -88,12 +84,6 @@ function getCompProps() { Record >; - const indexPatternList = [ - { id: '0', attributes: { title: 'b' } } as SavedObject, - { id: '1', attributes: { title: 'a' } } as SavedObject, - { id: '2', attributes: { title: 'c' } } as SavedObject, - ]; - const fieldCounts: Record = {}; for (const hit of hits) { @@ -105,44 +95,48 @@ function getCompProps() { columns: ['extension'], fieldCounts, hits, - indexPatternList, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(), - state: {}, + onReorderFields: jest.fn(), }; } describe('discover sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + it('should have Selected Fields and Available Fields with Popular Fields sections', async function () { + render(); - beforeAll(() => { - props = getCompProps(); - comp = mountWithIntl(); - }); + const popular = screen.getByTestId('fieldList-popular'); + const selected = screen.getByTestId('fieldList-selected'); + const unpopular = screen.getByTestId('fieldList-unpopular'); - it('should have Selected Fields and Available Fields with Popular Fields sections', function () { - const popular = findTestSubject(comp, 'fieldList-popular'); - const selected = findTestSubject(comp, 'fieldList-selected'); - const unpopular = findTestSubject(comp, 'fieldList-unpopular'); - expect(popular.children().length).toBe(1); - expect(unpopular.children().length).toBe(7); - expect(selected.children().length).toBe(1); + expect(within(popular).getAllByTestId('fieldList-field').length).toBe(1); + expect(within(unpopular).getAllByTestId('fieldList-field').length).toBe(7); + expect(within(selected).getAllByTestId('fieldList-field').length).toBe(1); }); - it('should allow selecting fields', function () { - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow selecting fields', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + it('should allow deselecting fields', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-extension')); + expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should allow adding filters', function () { - findTestSubject(comp, 'field-extension-showDetails').simulate('click'); - findTestSubject(comp, 'plus-extension-gif').simulate('click'); + it('should allow adding filters', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('field-extension-showDetails')); + await fireEvent.click(screen.getByTestId('plus-extension-gif')); expect(props.onAddFilter).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 865aff590286..f5e091e6ab82 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -31,14 +31,18 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@osd/i18n'; -import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { sortBy } from 'lodash'; +import { + EuiTitle, + EuiDragDropContext, + DropResult, + EuiDroppable, + EuiDraggable, + EuiPanel, + EuiSplitPanel, +} from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { DiscoverField } from './discover_field'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; @@ -61,13 +65,13 @@ export interface DiscoverSidebarProps { */ hits: Array>; /** - * List of available index patterns + * Callback function when selecting a field */ - indexPatternList: Array>; + onAddField: (fieldName: string, index?: number) => void; /** - * Callback function when selecting a field + * Callback function when rearranging fields */ - onAddField: (fieldName: string) => void; + onReorderFields: (sourceIdx: number, destinationIdx: number) => void; /** * Callback function when adding a filter from sidebar */ @@ -81,24 +85,18 @@ export interface DiscoverSidebarProps { * Currently selected index pattern */ selectedIndexPattern?: IndexPattern; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; } export function DiscoverSidebar({ columns, fieldCounts, hits, - indexPatternList, onAddField, onAddFilter, onRemoveField, + onReorderFields, selectedIndexPattern, - setIndexPattern, }: DiscoverSidebarProps) { - const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); const services = useMemo(() => getServices(), []); @@ -148,73 +146,109 @@ export function DiscoverSidebar({ return result; }, [fields]); + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (!source || !destination || !fields) return; + + // Rearranging fields within the selected fields list + if ( + source.droppableId === 'SELECTED_FIELDS' && + destination.droppableId === 'SELECTED_FIELDS' + ) { + onReorderFields(source.index, destination.index); + return; + } + // Dropping fields into the selected fields list + if ( + source.droppableId !== 'SELECTED_FIELDS' && + destination.droppableId === 'SELECTED_FIELDS' + ) { + const fieldListMap = { + POPULAR_FIELDS: popularFields, + UNPOPULAR_FIELDS: unpopularFields, + }; + const fieldList = fieldListMap[source.droppableId as keyof typeof fieldListMap]; + const field = fieldList[source.index]; + onAddField(field.name, destination.index); + return; + } + }, + [fields, onAddField, onReorderFields, popularFields, unpopularFields] + ); + if (!selectedIndexPattern || !fields) { return null; } return ( -
- o.attributes.title)} - /> -
-
+ + + - -
-
- {fields.length > 0 && ( - <> - -

- -

-
- -
    - {selectedFields.map((field: IndexPatternField) => { - return ( -
  • - -
  • - ); - })} -
-
- + + + {fields.length > 0 && ( + <> + +

+ +

+
+ + {selectedFields.map((field: IndexPatternField, index) => { + return ( + + + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + ); + })} + +

-
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> -
-
- - )} - {popularFields.length > 0 && ( -
- - - -
    - {popularFields.map((field: IndexPatternField) => { - return ( -
  • - -
  • - ); - })} -
-
- )} -
    - {unpopularFields.map((field: IndexPatternField) => { - return ( -
  • 0 && ( + + + + + + {popularFields.map((field: IndexPatternField, index) => { + return ( + + + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + ); + })} + + + )} + - -
  • - ); - })} -
-
-
+ {unpopularFields.map((field: IndexPatternField, index) => { + return ( + + + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + ); + })} + + + )} + + +
); } diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index fad1db402467..dff60827ccd2 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -83,5 +83,12 @@ export function groupFields( } } + // sort the selected fields by the column order + result.selected.sort((a, b) => { + const aIndex = columns.indexOf(a.name); + const bIndex = columns.indexOf(b.name); + return aIndex - bIndex; + }); + return result; } diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx index a1e5754cb312..a780752fc54c 100644 --- a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -49,7 +49,7 @@ export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { // prevent the anchor to reload the page on click event.preventDefault(); // The destinationId prop cannot be leveraged here as the table needs - // to be updated first (angular logic) + // to be updated first onClick(); }} className="dscSkipButton" diff --git a/src/plugins/discover/public/application/components/table/table.scss b/src/plugins/discover/public/application/components/table/table.scss new file mode 100644 index 000000000000..30ba5fea2a4e --- /dev/null +++ b/src/plugins/discover/public/application/components/table/table.scss @@ -0,0 +1,3 @@ +.truncate-by-height { + overflow: hidden; +} diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 90167a515985..3ef8e026702e 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -31,8 +31,9 @@ import React, { useState } from 'react'; import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; -import { arrayContainsObjects, trimAngularSpan } from './table_helper'; +import { arrayContainsObjects } from './table_helper'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; +import './table.scss'; const COLLAPSE_LINE_LENGTH = 350; @@ -61,7 +62,7 @@ export function DocViewTable({ .sort() .map((field) => { const valueRaw = flattened[field]; - const value = trimAngularSpan(String(formatted[field])); + const value = String(formatted[field]); const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; const isCollapsed = isCollapsible && !fieldRowOpen[field]; diff --git a/src/plugins/discover/public/application/components/table/table_helper.tsx b/src/plugins/discover/public/application/components/table/table_helper.tsx index 2e63b43b8310..12f50d5e79fb 100644 --- a/src/plugins/discover/public/application/components/table/table_helper.tsx +++ b/src/plugins/discover/public/application/components/table/table_helper.tsx @@ -34,10 +34,3 @@ export function arrayContainsObjects(value: unknown[]): boolean { return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null); } - -/** - * Removes markup added by OpenSearch Dashboards fields html formatter - */ -export function trimAngularSpan(text: string): string { - return text.replace(/^/, '').replace(/<\/span>$/, ''); -} diff --git a/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap rename to src/plugins/discover/public/application/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx new file mode 100644 index 000000000000..e893f9ee73c9 --- /dev/null +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx @@ -0,0 +1,339 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { DiscoverViewServices } from '../../../build_services'; +import { showOpenSearchPanel } from './show_open_search_panel'; +import { SavedSearch } from '../../../saved_searches'; +import { NEW_DISCOVER_APP } from '../../..'; +import { Adapters } from '../../../../../inspector/public'; +import { TopNavMenuData } from '../../../../../navigation/public'; +import { ISearchSource, unhashUrl } from '../../../opensearch_dashboards_services'; +import { + OnSaveProps, + SavedObjectSaveModal, + showSaveModal, +} from '../../../../../saved_objects/public'; +import { DiscoverState, setSavedSearchId } from '../../utils/state_management'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../common'; +import { getSortForSearchSource } from '../../view_components/utils/get_sort_for_search_source'; + +export const getTopNavLinks = ( + services: DiscoverViewServices, + inspectorAdapters: Adapters, + savedSearch: SavedSearch +) => { + const { + history, + inspector, + core, + uiSettings, + capabilities, + share, + toastNotifications, + chrome, + store, + } = services; + + const newSearch = { + id: 'new', + label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { + defaultMessage: 'New', + }), + description: i18n.translate('discover.localMenu.newSearchDescription', { + defaultMessage: 'New Search', + }), + run() { + setTimeout(() => { + history().push('/'); + // TODO: figure out why a history push doesn't update the app state. The page reload is a hack around it + window.location.reload(); + }, 0); + }, + testId: 'discoverNewButton', + }; + + const saveSearch: TopNavMenuData = { + id: 'save', + label: i18n.translate('discover.localMenu.saveTitle', { + defaultMessage: 'Save', + }), + description: i18n.translate('discover.localMenu.saveSearchDescription', { + defaultMessage: 'Save Search', + }), + testId: 'discoverSaveButton', + run: async () => { + const onSave = async ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }: OnSaveProps) => { + const currentTitle = savedSearch.title; + savedSearch.title = newTitle; + savedSearch.copyOnSave = newCopyOnSave; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + + const state: DiscoverState = store!.getState().discover; // store is defined before the view is loaded + + savedSearch.columns = state.columns; + savedSearch.sort = state.sort; + + try { + const id = await savedSearch.save(saveOptions); + + // If the title is a duplicate, the id will be an empty string. Checking for this condition here + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate('discover.notifications.savedSearchTitle', { + defaultMessage: `Search '{savedSearchTitle}' was saved`, + values: { + savedSearchTitle: savedSearch.title, + }, + }), + 'data-test-subj': 'saveSearchSuccess', + }); + + if (id !== state.savedSearch) { + setTimeout(() => { + history().push(`/view/${encodeURIComponent(id)}`); + // TODO: figure out why a history push doesn't update the app state. The page reload is a hack around it + window.location.reload(); + }, 0); + } else { + chrome.docTitle.change(savedSearch.lastSavedTitle); + chrome.setBreadcrumbs([ + { + text: i18n.translate('discover.discoverBreadcrumbTitle', { + defaultMessage: 'Discover', + }), + href: '#/', + }, + { text: savedSearch.title }, + ]); + } + + // set App state to clean + store!.dispatch({ type: setSavedSearchId.type, payload: id }); + } + } catch (error) { + toastNotifications.addDanger({ + title: i18n.translate('discover.notifications.notSavedSearchTitle', { + defaultMessage: `Search '{savedSearchTitle}' was not saved.`, + values: { + savedSearchTitle: savedSearch.title, + }, + }), + text: (error as Error).message, + }); + + // Reset the original title + savedSearch.title = currentTitle; + } + + return { test: true }; + }; + + const saveModal = ( + {}} + title={savedSearch.title} + showCopyOnSave={!!savedSearch.id} + objectType="search" + description={i18n.translate('discover.localMenu.saveSaveSearchDescription', { + defaultMessage: + 'Save your Discover search so you can use it in visualizations and dashboards', + })} + showDescription={false} + /> + ); + showSaveModal(saveModal, core.i18n.Context); + }, + }; + + const openSearch = { + id: 'open', + label: i18n.translate('discover.localMenu.openTitle', { + defaultMessage: 'Open', + }), + description: i18n.translate('discover.localMenu.openSavedSearchDescription', { + defaultMessage: 'Open Saved Search', + }), + testId: 'discoverOpenButton', + run: () => { + showOpenSearchPanel({ + makeUrl: (searchId) => `#/view/${encodeURIComponent(searchId)}`, + I18nContext: core.i18n.Context, + services, + }); + }, + }; + + const shareSearch: TopNavMenuData = { + id: 'share', + label: i18n.translate('discover.localMenu.shareTitle', { + defaultMessage: 'Share', + }), + description: i18n.translate('discover.localMenu.shareSearchDescription', { + defaultMessage: 'Share Search', + }), + testId: 'shareTopNavButton', + run: async (anchorElement) => { + const state: DiscoverState = store!.getState().discover; // store is defined before the view is loaded + const sharingData = await getSharingData({ + searchSource: savedSearch.searchSource, + state, + services, + }); + share?.toggleShareContextMenu({ + anchorElement, + allowEmbed: false, + allowShortUrl: capabilities.discover.createShortUrl as boolean, + shareableUrl: unhashUrl(window.location.href), + objectId: savedSearch.id, + objectType: 'search', + sharingData: { + ...sharingData, + title: savedSearch.title, + }, + isDirty: !savedSearch.id || state.isDirty, + }); + }, + }; + + const inspectSearch = { + id: 'inspect', + label: i18n.translate('discover.localMenu.inspectTitle', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('discover.localMenu.openInspectorForSearchDescription', { + defaultMessage: 'Open Inspector for search', + }), + testId: 'openInspectorButton', + run() { + inspector.open(inspectorAdapters, { + title: savedSearch?.title, + }); + }, + }; + + const legacyDiscover: TopNavMenuData = { + id: 'discover-new', + label: i18n.translate('discover.localMenu.newDiscoverTitle', { + defaultMessage: 'New Discover', + }), + description: i18n.translate('discover.localMenu.newDiscoverDescription', { + defaultMessage: 'New Discover Experience', + }), + testId: 'discoverNewButton', + run: async () => { + await uiSettings.set(NEW_DISCOVER_APP, false); + window.location.reload(); + }, + type: 'toggle' as const, + emphasize: true, + }; + + return [ + legacyDiscover, + newSearch, + ...(capabilities.discover.save ? [saveSearch] : []), + openSearch, + ...(share ? [shareSearch] : []), // Show share option only if share plugin is available + inspectSearch, + ]; +}; + +// TODO: This does not seem to affect the share menu. need to look into it in future +// const getFieldCounts = async () => { +// // the field counts aren't set until we have the data back, +// // so we wait for the fetch to be done before proceeding +// if ($scope.fetchStatus === fetchStatuses.COMPLETE) { +// return $scope.fieldCounts; +// } + +// return await new Promise((resolve) => { +// const unwatch = $scope.$watch('fetchStatus', (newValue) => { +// if (newValue === fetchStatuses.COMPLETE) { +// unwatch(); +// resolve($scope.fieldCounts); +// } +// }); +// }); +// }; + +const getSharingDataFields = async ( + selectedFields: string[], + hideTimeColumn: boolean, + timeFieldName?: string +) => { + if (selectedFields.length === 1 && selectedFields[0] === '_source') { + // const fieldCounts = await getFieldCounts(); + return { + searchFields: undefined, + // selectFields: keys(fieldCounts).sort(), + }; + } + + const fields = + timeFieldName && !hideTimeColumn ? [timeFieldName, ...selectedFields] : selectedFields; + return { + searchFields: fields, + selectFields: fields, + }; +}; + +const getSharingData = async ({ + searchSource, + state, + services, +}: { + searchSource: ISearchSource; + state: DiscoverState; + services: DiscoverViewServices; +}) => { + const searchSourceInstance = searchSource.createCopy(); + const indexPattern = await searchSourceInstance.getField('index'); + + const { searchFields, selectFields } = await getSharingDataFields( + state.columns, + services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING), + indexPattern?.timeFieldName + ); + + searchSourceInstance.setField('fields', searchFields); + searchSourceInstance.setField( + 'sort', + getSortForSearchSource( + state.sort, + indexPattern, + services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ) + ); + searchSourceInstance.setField('highlight', null); + searchSourceInstance.setField('highlightAll', undefined); + searchSourceInstance.setField('aggs', null); + searchSourceInstance.setField('size', undefined); + + const body = await searchSource.getSearchRequestBody(); + return { + searchRequest: { + index: indexPattern?.title, + body, + }, + // fields: selectFields, + metaFields: indexPattern?.metaFields, + conflictedTypesFields: indexPattern?.fields + .filter((f) => f.type === 'conflict') + .map((f) => f.name), + indexPatternId: indexPattern?.id, + }; +}; diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx b/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx new file mode 100644 index 000000000000..9dab50d341f8 --- /dev/null +++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.tsx @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +jest.mock('../../../../../saved_objects/public', () => ({ + SavedObjectFinderUi: () =>
, +})); + +jest.mock('../../../../../opensearch_dashboards_react/public', () => ({ + useOpenSearchDashboards: jest.fn().mockReturnValue({ + services: { + core: { uiSettings: {}, savedObjects: {} }, + addBasePath: (path: string) => path, + }, + }), + withOpenSearchDashboards: jest.fn(), +})); + +import { OpenSearchPanel } from './open_search_panel'; + +test('render', () => { + const component = shallow( {}} makeUrl={(id) => id} />); + expect(component).toMatchSnapshot(); +}); diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx new file mode 100644 index 000000000000..2dd5bb9bbe90 --- /dev/null +++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import rison from 'rison-node'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage } from '@osd/i18n/react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiFlyoutBody, + EuiTitle, +} from '@elastic/eui'; +import { SavedObjectFinderUi } from '../../../../../saved_objects/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverViewServices } from '../../../build_services'; +import { SAVED_OBJECT_TYPE } from '../../../saved_searches/_saved_search'; + +interface Props { + onClose: () => void; + makeUrl: (id: string) => string; +} + +export function OpenSearchPanel({ onClose, makeUrl }: Props) { + const { + services: { + core: { uiSettings, savedObjects }, + addBasePath, + }, + } = useOpenSearchDashboards(); + + return ( + + + +

+ +

+
+
+ + + } + savedObjectMetaData={[ + { + type: SAVED_OBJECT_TYPE, + getIconForSavedObject: () => 'search', + name: i18n.translate('discover.savedSearch.savedObjectName', { + defaultMessage: 'Saved search', + }), + }, + ]} + onChoose={(id) => { + setTimeout(() => { + window.location.assign(makeUrl(id)); + // TODO: figure out why a history push doesn't update the app state. The page reload is a hack around it + window.location.reload(); + onClose(); + }, 0); + }} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + + + + + +
+ ); +} diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx new file mode 100644 index 000000000000..5bc95b1b9cf2 --- /dev/null +++ b/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.tsx @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { OpenSearchPanel } from './open_search_panel'; +import { I18nStart } from '../../../../../../core/public'; +import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverViewServices } from '../../../build_services'; + +let isOpen = false; + +export function showOpenSearchPanel({ + makeUrl, + I18nContext, + services, +}: { + makeUrl: (id: string) => string; + I18nContext: I18nStart['Context']; + services: DiscoverViewServices; +}) { + if (isOpen) { + return; + } + + isOpen = true; + const container = document.createElement('div'); + const onClose = () => { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; + }; + + document.body.appendChild(container); + const element = ( + + + + + + ); + ReactDOM.render(element, container); +} diff --git a/src/plugins/discover/public/application/angular/directives/uninitialized.tsx b/src/plugins/discover/public/application/components/uninitialized/uninitialized.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/directives/uninitialized.tsx rename to src/plugins/discover/public/application/components/uninitialized/uninitialized.tsx diff --git a/src/plugins/discover/public/application/components/utils/use_pagination.test.ts b/src/plugins/discover/public/application/components/utils/use_pagination.test.ts new file mode 100644 index 000000000000..6eda1f7b4ca8 --- /dev/null +++ b/src/plugins/discover/public/application/components/utils/use_pagination.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + it('should initialize correctly with visParams and nRow', () => { + const nRow = 30; + const { result } = renderHook(() => usePagination(nRow)); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 100, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + pageSizeOptions: [25, 50, 100], + }); + }); + + it('should update pageSize correctly when calling onChangeItemsPerPage', () => { + const nRow = 30; + const { result } = renderHook(() => usePagination(nRow)); + + act(() => { + result.current?.onChangeItemsPerPage(20); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 20, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + pageSizeOptions: [25, 50, 100], + }); + }); + + it('should update pageIndex correctly when calling onChangePage', () => { + const nRow = 30; + const { result } = renderHook(() => usePagination(nRow)); + + act(() => { + result.current?.onChangePage(1); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 100, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + pageSizeOptions: [25, 50, 100], + }); + }); + + it('should correct pageIndex if it exceeds maximum page index after nRow or perPage change', () => { + const nRow = 300; + const { result } = renderHook(() => usePagination(nRow)); + + act(() => { + result.current?.onChangePage(4); + }); + + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 100, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + pageSizeOptions: [25, 50, 100], + }); + }); +}); diff --git a/src/plugins/discover/public/application/components/utils/use_pagination.ts b/src/plugins/discover/public/application/components/utils/use_pagination.ts new file mode 100644 index 000000000000..98363e57ed95 --- /dev/null +++ b/src/plugins/discover/public/application/components/utils/use_pagination.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useMemo, useCallback } from 'react'; + +export const usePagination = (rowCount: number) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 100 }); + const pageCount = useMemo(() => Math.ceil(rowCount / pagination.pageSize), [ + rowCount, + pagination, + ]); + + const onChangeItemsPerPage = useCallback( + (pageSize: number) => setPagination((p) => ({ ...p, pageSize })), + [] + ); + + const onChangePage = useCallback( + (pageIndex: number) => setPagination((p) => ({ ...p, pageIndex })), + [] + ); + + return useMemo( + () => + pagination.pageSize + ? { + ...pagination, + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: [25, 50, 100], // TODO: make this configurable + } + : undefined, + [pagination, onChangeItemsPerPage, onChangePage, pageCount] + ); +}; diff --git a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts index 56f167b5f2cc..904d3813cd69 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts @@ -28,32 +28,16 @@ * under the License. */ -import { auto } from 'angular'; -import { convertDirectiveToRenderFn } from './doc_views_helpers'; import { DocView, DocViewInput, OpenSearchSearchHit, DocViewInputFn } from './doc_views_types'; export class DocViewsRegistry { private docViews: DocView[] = []; - private angularInjectorGetter: (() => Promise) | null = null; - - setAngularInjectorGetter = (injectorGetter: () => Promise) => { - this.angularInjectorGetter = injectorGetter; - }; /** * Extends and adds the given doc view to the registry array */ addDocView(docViewRaw: DocViewInput | DocViewInputFn) { const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; - if (docView.directive) { - // convert angular directive to render function for backwards compatibility - docView.render = convertDirectiveToRenderFn(docView.directive, () => { - if (!this.angularInjectorGetter) { - throw new Error('Angular was not initialized'); - } - return this.angularInjectorGetter(); - }); - } if (typeof docView.shouldShow !== 'function') { docView.shouldShow = () => true; } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 961fc98516f6..db9757d385b0 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -29,17 +29,9 @@ */ import { ComponentType } from 'react'; -import { IScope } from 'angular'; import { SearchResponse } from 'elasticsearch'; import { IndexPattern } from '../../../../data/public'; -export interface AngularDirective { - controller: (...injectedServices: any[]) => void; - template: string; -} - -export type AngularScope = IScope; - export type OpenSearchSearchHit = SearchResponse['hits']['hits'][number]; export interface FieldMapping { @@ -72,7 +64,6 @@ export type DocViewRenderFn = ( export interface DocViewInput { component?: DocViewerComponent; - directive?: AngularDirective; order: number; render?: DocViewRenderFn; shouldShow?: (hit: OpenSearchSearchHit) => boolean; diff --git a/src/plugins/discover/public/application/utils/columns.test.ts b/src/plugins/discover/public/application/utils/columns.test.ts new file mode 100644 index 000000000000..43c4b0555553 --- /dev/null +++ b/src/plugins/discover/public/application/utils/columns.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { buildColumns } from './columns'; + +describe('buildColumns', () => { + it('returns ["_source"] if columns is empty', () => { + expect(buildColumns([])).toEqual(['_source']); + }); + + it('returns columns if there is only one column', () => { + expect(buildColumns(['foo'])).toEqual(['foo']); + }); + + it('removes "_source" if there are more than one columns', () => { + expect(buildColumns(['foo', '_source', 'bar'])).toEqual(['foo', 'bar']); + }); + + it('returns columns if there are more than one columns but no "_source"', () => { + expect(buildColumns(['foo', 'bar'])).toEqual(['foo', 'bar']); + }); +}); diff --git a/src/plugins/discover/public/application/utils/columns.ts b/src/plugins/discover/public/application/utils/columns.ts new file mode 100644 index 000000000000..062ca24e3ba4 --- /dev/null +++ b/src/plugins/discover/public/application/utils/columns.ts @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Helper function to provide a fallback to a single _source column if the given array of columns + * is empty, and removes _source if there are more than 1 columns given + * @param columns + */ +export function buildColumns(columns: string[]) { + if (columns.length > 1 && columns.indexOf('_source') !== -1) { + return columns.filter((col) => col !== '_source'); + } else if (columns.length !== 0) { + return columns; + } + return ['_source']; +} diff --git a/src/plugins/discover/public/application/utils/state_management/common.test.ts b/src/plugins/discover/public/application/utils/state_management/common.test.ts new file mode 100644 index 000000000000..c1190f891648 --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/common.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { addColumn, removeColumn, reorderColumn, setColumns } from './common'; + +describe('commonUtils', () => { + it('should handle addColumn', () => { + expect(addColumn(['column1'], { column: 'column2' })).toEqual(['column1', 'column2']); + expect(addColumn(['column1'], { column: 'column2', index: 0 })).toEqual(['column2', 'column1']); + }); + + it('should handle removeColumn', () => { + expect(removeColumn(['column1', 'column2'], 'column1')).toEqual(['column2']); + }); + + it('should handle reorderColumn', () => { + expect(reorderColumn(['column1', 'column2', 'column3'], 0, 2)).toEqual([ + 'column2', + 'column3', + 'column1', + ]); + }); + + it('should handle setColumns', () => { + expect(setColumns('timeField', ['timeField', 'column1', 'column2'])).toEqual([ + 'column1', + 'column2', + ]); + expect(setColumns(undefined, ['column1', 'column2'])).toEqual(['column1', 'column2']); + }); +}); diff --git a/src/plugins/discover/public/application/utils/state_management/common.ts b/src/plugins/discover/public/application/utils/state_management/common.ts new file mode 100644 index 000000000000..566ee6e468ff --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/common.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const addColumn = (columns: string[], action: { column: string; index?: number }) => { + const { column, index } = action; + const newColumns = [...(columns || [])]; + if (index !== undefined) newColumns.splice(index, 0, column); + else newColumns.push(column); + return newColumns; +}; + +export const removeColumn = (columns: string[], actionColumn: string) => { + return (columns || []).filter((column) => column !== actionColumn); +}; + +export const reorderColumn = (columns: string[], source: number, destination: number) => { + const newColumns = [...(columns || [])]; + const [removed] = newColumns.splice(source, 1); + newColumns.splice(destination, 0, removed); + return newColumns; +}; + +export const setColumns = (timeField: string | undefined, columns: string[]) => { + const newColumns = timeField && timeField === columns[0] ? columns.slice(1) : columns; + return newColumns; +}; diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx new file mode 100644 index 000000000000..1e2e4ab46a55 --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { discoverSlice, DiscoverState } from './discover_slice'; + +describe('discoverSlice', () => { + let initialState: DiscoverState; + + beforeEach(() => { + initialState = { + columns: [], + sort: [], + }; + }); + + it('should handle setState', () => { + const newState = { + columns: ['column1', 'column2'], + sort: [['field1', 'asc']], + }; + const action = { type: 'discover/setState', payload: newState }; + const result = discoverSlice.reducer(initialState, action); + expect(result).toEqual(newState); + }); + + it('should handle addColumn', () => { + const action1 = { type: 'discover/addColumn', payload: { column: 'column1' } }; + const result1 = discoverSlice.reducer(initialState, action1); + expect(result1.columns).toEqual(['column1']); + }); + + it('should handle removeColumn', () => { + initialState = { + columns: ['column1', 'column2'], + sort: [['column1', 'asc']], + }; + const action = { type: 'discover/removeColumn', payload: 'column1' }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column2']); + expect(result.sort).toEqual([]); + }); + + it('should handle reorderColumn', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { + type: 'discover/reorderColumn', + payload: { source: 0, destination: 2 }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column2', 'column3', 'column1']); + }); + + it('should handle setColumns', () => { + const action = { + type: 'discover/setColumns', + payload: { timeField: 'timeField', columns: ['timeField', 'column1', 'column2'] }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column1', 'column2']); + }); + + it('should handle setSort', () => { + const action = { type: 'discover/setSort', payload: [['field1', 'asc']] }; + const result = discoverSlice.reducer(initialState, action); + expect(result.sort).toEqual([['field1', 'asc']]); + }); + + it('should handle updateState', () => { + initialState = { + columns: ['column1', 'column2'], + sort: [['field1', 'asc']], + }; + const action = { + type: 'discover/updateState', + payload: { sort: [['field2', 'desc']] }, + }; + const result = discoverSlice.reducer(initialState, action); + expect(result.sort).toEqual([['field2', 'desc']]); + }); +}); diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx new file mode 100644 index 000000000000..73cc1af35e13 --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -0,0 +1,166 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { matchPath } from 'react-router-dom'; +import { Filter, Query } from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { RootState, DefaultViewState } from '../../../../../data_explorer/public'; +import { buildColumns } from '../columns'; +import * as utils from './common'; +import { SortOrder } from '../../../saved_searches/types'; + +export interface DiscoverState { + /** + * Columns displayed in the table + */ + columns: string[]; + /** + * Array of applied filters + */ + filters?: Filter[]; + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Lucence or DQL query + */ + query?: Query; + /** + * Array of the used sorting [[field,direction],...] + */ + sort: SortOrder[]; + /** + * id of the used saved search + */ + savedSearch?: string; + /** + * dirty flag to indicate if the saved search has been modified + * since the last save + */ + isDirty: boolean; +} + +export interface DiscoverRootState extends RootState { + discover: DiscoverState; +} + +const initialState: DiscoverState = { + columns: ['_source'], + sort: [], + isDirty: false, +}; + +export const getPreloadedState = async ({ + getSavedSearchById, +}: DiscoverServices): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; + + const hashPath = window.location.hash.split('?')[0]; // hack to remove query params since matchPath considers them part of the id + const savedSearchId = matchPath<{ id?: string }>(hashPath, { + path: '#/view/:id', + })?.params.id; + + if (savedSearchId) { + const savedSearchInstance = await getSavedSearchById(savedSearchId); + + if (savedSearchInstance) { + preloadedState.state.columns = savedSearchInstance.columns; + preloadedState.state.sort = savedSearchInstance.sort; + preloadedState.state.savedSearch = savedSearchInstance.id; + const indexPatternId = savedSearchInstance.searchSource.getField('index')?.id; + preloadedState.root = { + metadata: { + indexPattern: indexPatternId, + }, + }; + + savedSearchInstance.destroy(); // this instance is no longer needed, will create another one later + } + } + + return preloadedState; +}; + +export const discoverSlice = createSlice({ + name: 'discover', + initialState, + reducers: { + setState(state, action: PayloadAction) { + return action.payload; + }, + addColumn(state, action: PayloadAction<{ column: string; index?: number }>) { + const columns = utils.addColumn(state.columns || [], action.payload); + return { ...state, columns: buildColumns(columns) }; + }, + removeColumn(state, action: PayloadAction) { + const columns = utils.removeColumn(state.columns, action.payload); + const sort = + state.sort && state.sort.length ? state.sort.filter((s) => s[0] !== action.payload) : []; + return { + ...state, + columns: buildColumns(columns), + sort, + isDirty: true, + }; + }, + reorderColumn(state, action: PayloadAction<{ source: number; destination: number }>) { + const columns = utils.reorderColumn( + state.columns, + action.payload.source, + action.payload.destination + ); + return { + ...state, + columns, + isDirty: true, + }; + }, + setColumns(state, action: PayloadAction<{ timeField: string | undefined; columns: string[] }>) { + const columns = utils.setColumns(action.payload.timeField, action.payload.columns); + return { + ...state, + columns, + }; + }, + setSort(state, action: PayloadAction) { + return { + ...state, + sort: action.payload, + }; + }, + updateState(state, action: PayloadAction>) { + return { + ...state, + ...action.payload, + }; + }, + setSavedSearchId(state, action: PayloadAction) { + return { + ...state, + savedSearch: action.payload, + isDirty: false, + }; + }, + }, +}); + +// Exposing the state functions as generics +export const { + addColumn, + removeColumn, + reorderColumn, + setColumns, + setSort, + setState, + updateState, + setSavedSearchId, +} = discoverSlice.actions; +export const { reducer } = discoverSlice; diff --git a/src/plugins/discover/public/application/utils/state_management/index.ts b/src/plugins/discover/public/application/utils/state_management/index.ts new file mode 100644 index 000000000000..d72cc772e6c4 --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TypedUseSelectorHook } from 'react-redux'; +import { RootState, useTypedDispatch, useTypedSelector } from '../../../../../data_explorer/public'; +import { DiscoverState } from './discover_slice'; + +export * from './discover_slice'; + +export interface DiscoverRootState extends RootState { + discover: DiscoverState; +} + +export const useSelector: TypedUseSelectorHook = useTypedSelector; +export const useDispatch = useTypedDispatch; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.scss b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.scss new file mode 100644 index 000000000000..d0d0951c67b9 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.scss @@ -0,0 +1,25 @@ +.dscResultCount { + padding-top: $euiSizeXS; +} + +.dscTimechart { + display: block; + position: relative; + + // SASSTODO: the visualizing component should have an option or a modifier + .series > rect { + fill-opacity: 0.5; + stroke-width: 1; + } +} + +.dscHistogram { + display: flex; + height: 200px; + padding: $euiSizeS; +} + +.dscHistogram__header--partial { + font-weight: $euiFontWeightRegular; + min-width: $euiSize * 12; +} diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx new file mode 100644 index 000000000000..db3ad027c9c2 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import './discover_chart_container.scss'; +import React from 'react'; +import { DiscoverViewServices } from '../../../build_services'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { useDiscoverContext } from '../context'; +import { SearchData } from '../utils/use_search'; +import { DiscoverChart } from '../../components/chart/chart'; + +export const DiscoverChartContainer = ({ hits, bucketInterval, chartData }: SearchData) => { + const { services } = useOpenSearchDashboards(); + const { uiSettings, data } = services; + const { indexPattern, savedSearch } = useDiscoverContext(); + + const timeField = indexPattern?.timeFieldName; + + if (!hits || !bucketInterval || !chartData) { + // TODO: handle better + return null; + } + + return ( + { + window.location.href = `#/view/${savedSearch?.id}`; + window.location.reload(); + }} + services={services} + showResetButton={!!savedSearch && !!savedSearch.id} + /> + ); +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx new file mode 100644 index 000000000000..f4e28a59b08d --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { History } from 'history'; +import { DiscoverViewServices } from '../../../build_services'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DataGridTable } from '../../components/data_grid/data_grid_table'; +import { useDiscoverContext } from '../context'; +import { + addColumn, + removeColumn, + setColumns, + setSort, + useDispatch, + useSelector, +} from '../../utils/state_management'; +import { ResultStatus, SearchData } from '../utils/use_search'; +import { IndexPatternField, opensearchFilters } from '../../../../../data/public'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { SortOrder } from '../../../saved_searches/types'; + +interface Props { + history: History; +} + +export const DiscoverTable = ({ history }: Props) => { + const { services } = useOpenSearchDashboards(); + const { filterManager } = services.data.query; + const { data$, indexPattern } = useDiscoverContext(); + const [fetchState, setFetchState] = useState({ + status: data$.getValue().status, + rows: [], + }); + + const { columns, sort } = useSelector((state) => state.discover); + const dispatch = useDispatch(); + const onAddColumn = (col: string) => dispatch(addColumn({ column: col })); + const onRemoveColumn = (col: string) => dispatch(removeColumn(col)); + const onSetColumns = (cols: string[]) => + dispatch(setColumns({ timefield: indexPattern.timeFieldName, columns: cols })); + const onSetSort = (s: SortOrder[]) => dispatch(setSort(s)); + const onAddFilter = useCallback( + (field: IndexPatternField, values: string, operation: '+' | '-') => { + const newFilters = opensearchFilters.generateFilters( + filterManager, + field, + values, + operation, + indexPattern.id + ); + return filterManager.addFilters(newFilters); + }, + [filterManager, indexPattern] + ); + + const { rows } = fetchState || {}; + + useEffect(() => { + const subscription = data$.subscribe((next) => { + if (next.status === ResultStatus.LOADING) return; + if (next.status !== fetchState.status || (next.rows && next.rows !== fetchState.rows)) { + setFetchState({ ...fetchState, ...next }); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + + if (indexPattern === undefined) { + // TODO: handle better + return null; + } + + if (!rows || rows.length === 0) { + // TODO: handle better + return
{'loading...'}
; + } + + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx new file mode 100644 index 000000000000..8caa65af54d6 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { TopNav } from './top_nav'; +import { ViewProps } from '../../../../../data_explorer/public'; +import { DiscoverTable } from './discover_table'; +import { DiscoverChartContainer } from './discover_chart_container'; +import { useDiscoverContext } from '../context'; +import { ResultStatus, SearchData } from '../utils/use_search'; +import { DiscoverNoResults } from '../../components/no_results/no_results'; +import { DiscoverUninitialized } from '../../components/uninitialized/uninitialized'; +import { LoadingSpinner } from '../../components/loading_spinner/loading_spinner'; + +// eslint-disable-next-line import/no-default-export +export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewProps) { + const { data$, refetch$, indexPattern } = useDiscoverContext(); + + const [fetchState, setFetchState] = useState({ + status: data$.getValue().status, + hits: 0, + bucketInterval: {}, + }); + + const { status } = fetchState; + + useEffect(() => { + const subscription = data$.subscribe((next) => { + if ( + next.status !== fetchState.status || + (next.hits && next.hits !== fetchState.hits) || + (next.bucketInterval && next.bucketInterval !== fetchState.bucketInterval) || + (next.chartData && next.chartData !== fetchState.chartData) + ) { + setFetchState({ ...fetchState, ...next }); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + + const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; + + return ( + + + + + {status === ResultStatus.NO_RESULTS && ( + + + + )} + {status === ResultStatus.UNINITIALIZED && ( + refetch$.next()} /> + )} + {status === ResultStatus.LOADING && } + {status === ResultStatus.READY && ( + <> + + + + + + + + + + + + )} + + ); +} diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx new file mode 100644 index 000000000000..d45e9909b322 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { AppMountParameters } from '../../../../../../core/public'; +import { NEW_DISCOVER_APP, PLUGIN_ID } from '../../../../common'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverViewServices } from '../../../build_services'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { getTopNavLinks } from '../../components/top_nav/get_top_nav_links'; +import { useDiscoverContext } from '../context'; + +export interface TopNavProps { + opts: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + }; +} + +export const TopNav = ({ opts }: TopNavProps) => { + const { services } = useOpenSearchDashboards(); + const { inspectorAdapters, savedSearch } = useDiscoverContext(); + const [indexPatterns, setIndexPatterns] = useState(undefined); + + const { + uiSettings, + navigation: { + ui: { TopNavMenu }, + }, + core: { + application: { navigateToApp }, + }, + data, + } = services; + + const topNavLinks = savedSearch ? getTopNavLinks(services, inspectorAdapters, savedSearch) : []; + + useEffect(() => { + if (uiSettings.get(NEW_DISCOVER_APP) === false) { + const path = window.location.hash; + navigateToApp('discoverLegacy', { + replace: true, + path, + }); + } + + return () => {}; + }, [navigateToApp, uiSettings]); + + useEffect(() => { + let isMounted = true; + const getDefaultIndexPattern = async () => { + await data.indexPatterns.ensureDefaultIndexPattern(); + const indexPattern = await data.indexPatterns.getDefault(); + + if (!isMounted) return; + + setIndexPatterns(indexPattern ? [indexPattern] : undefined); + }; + + getDefaultIndexPattern(); + + return () => { + isMounted = false; + }; + }, [data.indexPatterns]); + + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/view_components/context/index.tsx b/src/plugins/discover/public/application/view_components/context/index.tsx new file mode 100644 index 000000000000..29daca731714 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/context/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { DataExplorerServices, ViewProps } from '../../../../../data_explorer/public'; +import { + OpenSearchDashboardsContextProvider, + useOpenSearchDashboards, +} from '../../../../../opensearch_dashboards_react/public'; +import { getServices } from '../../../opensearch_dashboards_services'; +import { useSearch, SearchContextValue } from '../utils/use_search'; +import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; + +const SearchContext = React.createContext({} as SearchContextValue); + +// eslint-disable-next-line import/no-default-export +export default function DiscoverContext({ children }: React.PropsWithChildren) { + const services = getServices(); + const searchParams = useSearch(services); + + const { + services: { osdUrlStateStorage }, + } = useOpenSearchDashboards(); + + // Connect the query service to the url state + useEffect(() => { + connectStorageToQueryState(services.data.query, osdUrlStateStorage, { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + }); + }, [osdUrlStateStorage, services.data.query, services.uiSettings]); + + return ( + + {children} + + ); +} + +export const useDiscoverContext = () => React.useContext(SearchContext); diff --git a/src/plugins/discover/public/application/view_components/index.ts b/src/plugins/discover/public/application/view_components/index.ts new file mode 100644 index 000000000000..45fd68cf1285 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './canvas'; +export * from './panel'; diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx new file mode 100644 index 000000000000..6f2e04b9a9ba --- /dev/null +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { ViewProps } from '../../../../../data_explorer/public'; +import { + addColumn, + removeColumn, + reorderColumn, + useDispatch, + useSelector, +} from '../../utils/state_management'; +import { DiscoverSidebar } from '../../components/sidebar'; +import { useDiscoverContext } from '../context'; +import { ResultStatus, SearchData } from '../utils/use_search'; + +// eslint-disable-next-line import/no-default-export +export default function DiscoverPanel(props: ViewProps) { + const { data$, indexPattern } = useDiscoverContext(); + const [fetchState, setFetchState] = useState(data$.getValue()); + + const { columns } = useSelector((state) => ({ + columns: state.discover.columns, + })); + const dispatch = useDispatch(); + + useEffect(() => { + const subscription = data$.subscribe((next) => { + if (next.status === ResultStatus.LOADING) return; + setFetchState(next); + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + + return ( + { + dispatch( + addColumn({ + column: fieldName, + index, + }) + ); + }} + onRemoveField={(fieldName) => { + dispatch(removeColumn(fieldName)); + }} + onReorderFields={(source, destination) => { + dispatch( + reorderColumn({ + source, + destination, + }) + ); + }} + selectedIndexPattern={indexPattern} + onAddFilter={() => {}} + /> + ); +} diff --git a/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts b/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts new file mode 100644 index 000000000000..584e47047c57 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/get_default_sort.ts @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../opensearch_dashboards_services'; +// @ts-ignore +import { isSortable } from './get_sort'; + +export type SortOrder = [string, string]; + +/** + * use in case the user didn't manually sort. + * the default sort is returned depending of the index pattern + */ +export function getDefaultSort( + indexPattern: IndexPattern, + defaultSortOrder: string = 'desc' +): SortOrder[] { + if (indexPattern.timeFieldName && isSortable(indexPattern.timeFieldName, indexPattern)) { + return [[indexPattern.timeFieldName, defaultSortOrder]]; + } else { + return [['_score', defaultSortOrder]]; + } +} diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort.test.ts b/src/plugins/discover/public/application/view_components/utils/get_sort.test.ts new file mode 100644 index 000000000000..f1b4861e4c94 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/get_sort.test.ts @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getSort, getSortArray } from './get_sort'; +// @ts-ignore +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; + +describe('docTable', function () { + let indexPattern: IndexPattern; + + beforeEach(() => { + indexPattern = FixturesStubbedLogstashIndexPatternProvider() as IndexPattern; + }); + + describe('getSort function', function () { + test('should be a function', function () { + expect(typeof getSort === 'function').toBeTruthy(); + }); + + test('should return an array of objects', function () { + expect(getSort([['bytes', 'desc']], indexPattern)).toEqual([{ bytes: 'desc' }]); + + delete indexPattern.timeFieldName; + expect(getSort([['bytes', 'desc']], indexPattern)).toEqual([{ bytes: 'desc' }]); + }); + + test('should passthrough arrays of objects', () => { + expect(getSort([{ bytes: 'desc' }], indexPattern)).toEqual([{ bytes: 'desc' }]); + }); + + test('should return an empty array when passed an unsortable field', function () { + expect(getSort([['non-sortable', 'asc']], indexPattern)).toEqual([]); + expect(getSort([['lol_nope', 'asc']], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSort([['non-sortable', 'asc']], indexPattern)).toEqual([]); + }); + + test('should return an empty array ', function () { + expect(getSort([], indexPattern)).toEqual([]); + expect(getSort([['foo', 'bar']], indexPattern)).toEqual([]); + expect(getSort([{ foo: 'bar' }], indexPattern)).toEqual([]); + }); + + test('should convert a legacy sort to an array of objects', function () { + expect(getSort(['foo', 'desc'], indexPattern)).toEqual([{ foo: 'desc' }]); + expect(getSort(['foo', 'asc'], indexPattern)).toEqual([{ foo: 'asc' }]); + }); + }); + + describe('getSortArray function', function () { + test('should have an array method', function () { + expect(getSortArray).toBeInstanceOf(Function); + }); + + test('should return an array of arrays for sortable fields', function () { + expect(getSortArray([['bytes', 'desc']], indexPattern)).toEqual([['bytes', 'desc']]); + }); + + test('should return an array of arrays from an array of elasticsearch sort objects', function () { + expect(getSortArray([{ bytes: 'desc' }], indexPattern)).toEqual([['bytes', 'desc']]); + }); + + test('should sort by an empty array when an unsortable field is given', function () { + expect(getSortArray([{ 'non-sortable': 'asc' }], indexPattern)).toEqual([]); + expect(getSortArray([{ lol_nope: 'asc' }], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSortArray([{ 'non-sortable': 'asc' }], indexPattern)).toEqual([]); + }); + + test('should return an empty array when passed an empty sort array', () => { + expect(getSortArray([], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSortArray([], indexPattern)).toEqual([]); + }); + }); +}); diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort.ts b/src/plugins/discover/public/application/view_components/utils/get_sort.ts new file mode 100644 index 000000000000..32de9e352f88 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/get_sort.ts @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; + +export type SortPairObj = Record; +export type SortPairArr = [string, string]; +export type SortPair = SortPairArr | SortPairObj; +export type SortInput = SortPair | SortPair[]; + +export function isSortable(fieldName: string, indexPattern: IndexPattern) { + const field = indexPattern.getFieldByName(fieldName); + return field && field.sortable; +} + +function createSortObject( + sortPair: SortInput, + indexPattern: IndexPattern +): SortPairObj | undefined { + if ( + Array.isArray(sortPair) && + sortPair.length === 2 && + isSortable(String(sortPair[0]), indexPattern) + ) { + const [field, direction] = sortPair as SortPairArr; + return { [field]: direction }; + } else if (_.isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { + return sortPair as SortPairObj; + } +} + +export function isLegacySort(sort: SortPair[] | SortPair): sort is SortPair { + return ( + sort.length === 2 && typeof sort[0] === 'string' && (sort[1] === 'desc' || sort[1] === 'asc') + ); +} + +/** + * Take a sorting array and make it into an object + * @param {array} sort two dimensional array [[fieldToSort, directionToSort]] + * or an array of objects [{fieldToSort: directionToSort}] + * @param {object} indexPattern used for determining default sort + * @returns Array<{object}> an array of sort objects + */ +export function getSort(sort: SortPair[] | SortPair, indexPattern: IndexPattern): SortPairObj[] { + if (Array.isArray(sort)) { + if (isLegacySort(sort)) { + // To stay compatible with legacy sort, which just supported a single sort field + return [{ [sort[0]]: sort[1] }]; + } + return sort + .map((sortPair: SortPair) => createSortObject(sortPair, indexPattern)) + .filter((sortPairObj) => typeof sortPairObj === 'object') as SortPairObj[]; + } + return []; +} + +/** + * compared to getSort it doesn't return an array of objects, it returns an array of arrays + * [[fieldToSort: directionToSort]] + */ +export function getSortArray(sort: SortPair[], indexPattern: IndexPattern) { + return getSort(sort, indexPattern).map((sortPair) => Object.entries(sortPair).pop()); +} diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts new file mode 100644 index 000000000000..b19128a432e0 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OpenSearchQuerySortValue, IndexPattern } from '../../../opensearch_dashboards_services'; +import { getSort } from './get_sort'; +import { getDefaultSort } from './get_default_sort'; + +export type SortOrder = [string, string]; + +/** + * Prepares sort for search source, that's sending the request to OpenSearch + * - Adds default sort if necessary + * - Handles the special case when there's sorting by date_nanos typed fields + * the addon of the numeric_type guarantees the right sort order + * when there are indices with date and indices with date_nanos field + */ +export function getSortForSearchSource( + sort?: SortOrder[], + indexPattern?: IndexPattern, + defaultDirection: string = 'desc' +): OpenSearchQuerySortValue[] { + if (!sort || !indexPattern) { + return []; + } else if (Array.isArray(sort) && sort.length === 0) { + sort = getDefaultSort(indexPattern, defaultDirection); + } + const { timeFieldName } = indexPattern; + return getSort(sort, indexPattern).map((sortPair: Record) => { + if (indexPattern.isTimeNanosBased() && timeFieldName && sortPair[timeFieldName]) { + return { + [timeFieldName]: { + order: sortPair[timeFieldName], + numeric_type: 'date_nanos', + }, + } as OpenSearchQuerySortValue; + } + return sortPair as OpenSearchQuerySortValue; + }); +} diff --git a/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts b/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts new file mode 100644 index 000000000000..d6285594367e --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { SearchSource, IndexPattern } from 'src/plugins/data/public'; +import { SavedObject, ToastsStart } from 'opensearch-dashboards/public'; +import { redirectWhenMissing, getUrlTracker } from '../../../opensearch_dashboards_services'; +import { getIndexPatternId } from '../../helpers/get_index_pattern_id'; + +export type IndexPatternSavedObject = SavedObject & { title: string }; +export interface IndexPatternData { + loaded: IndexPattern; + stateVal: string; + stateValFound: boolean; +} + +export const fetchIndexPattern = async (data, config) => { + await data.indexPatterns.ensureDefaultIndexPattern(); + const indexPatternList = await data.indexPatterns.getCache(); + const id = getIndexPatternId('', indexPatternList, config.get('defaultIndex')); + const indexPatternData = await data.indexPatterns.get(id); + const ip: IndexPatternData = { + loaded: indexPatternData, + stateVal: '', // TODO: get stateVal from appStateContainer + stateValFound: false, // TODO: get stateValFound from appStateContainer + }; + return ip; +}; + +export const fetchSavedSearch = async ( + core, + basePath, + history, + savedSearchId, + services, + toastNotifications +) => { + try { + const savedSearch = await services.getSavedSearchById(savedSearchId); + return savedSearch; + } catch (error) { + // TODO: handle redirect with Data Explorer + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `opensearch-dashboards/objects/savedSearches/${savedSearchId}`, + }, + }, + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + }); + } +}; + +export function resolveIndexPattern( + ip: IndexPatternData, + searchSource: SearchSource, + toastNotifications: ToastsStart +) { + const { loaded: loadedIndexPattern, stateVal, stateValFound } = ip; + + const ownIndexPattern = searchSource.getOwnField('index'); + + if (ownIndexPattern && !stateVal) { + return ownIndexPattern; + } + + if (stateVal && !stateValFound) { + const warningTitle = i18n.translate('discover.valueIsNotConfiguredIndexPatternIDWarningTitle', { + defaultMessage: '{stateVal} is not a configured index pattern ID', + values: { + stateVal: `"${stateVal}"`, + }, + }); + + if (ownIndexPattern) { + toastNotifications.addWarning({ + title: warningTitle, + text: i18n.translate('discover.showingSavedIndexPatternWarningDescription', { + defaultMessage: + 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})', + values: { + ownIndexPatternTitle: ownIndexPattern.title, + ownIndexPatternId: ownIndexPattern.id, + }, + }), + }); + return ownIndexPattern; + } + + toastNotifications.addWarning({ + title: warningTitle, + text: i18n.translate('discover.showingDefaultIndexPatternWarningDescription', { + defaultMessage: + 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})', + values: { + loadedIndexPatternTitle: loadedIndexPattern.title, + loadedIndexPatternId: loadedIndexPattern.id, + }, + }), + }); + } + + return loadedIndexPattern; +} diff --git a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts new file mode 100644 index 000000000000..1404773eb9d4 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + IndexPattern, + ISearchSource, + indexPatterns as indexPatternUtils, + AggConfigs, +} from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { SortOrder } from '../../../saved_searches/types'; +import { getSortForSearchSource } from './get_sort_for_search_source'; +import { SORT_DEFAULT_ORDER_SETTING, SAMPLE_SIZE_SETTING } from '../../../../common'; + +interface Props { + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[] | undefined; + searchSource?: ISearchSource; + histogramConfigs?: AggConfigs; +} + +export const updateSearchSource = async ({ + indexPattern, + services, + searchSource, + sort, + histogramConfigs, +}: Props) => { + const { uiSettings, data } = services; + const sortForSearchSource = getSortForSearchSource( + sort, + indexPattern, + uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ); + const size = uiSettings.get(SAMPLE_SIZE_SETTING); + const filters = data.query.filterManager.getFilters(); + + const searchSourceInstance = searchSource || (await data.search.searchSource.create()); + + // searchSource which applies time range + const timeRangeSearchSource = await data.search.searchSource.create(); + const { isDefault } = indexPatternUtils; + if (isDefault(indexPattern)) { + const timefilter = data.query.timefilter.timefilter; + + timeRangeSearchSource.setField('filter', () => { + return timefilter.createFilter(indexPattern); + }); + } + + searchSourceInstance.setParent(timeRangeSearchSource); + + searchSourceInstance.setFields({ + index: indexPattern, + sort: sortForSearchSource, + size, + query: data.query.queryString.getQuery() || null, + highlightAll: true, + version: true, + filter: filters, + }); + + if (histogramConfigs) { + const dslAggs = histogramConfigs.toDsl(); + searchSourceInstance.setField('aggs', dslAggs); + } + + return searchSourceInstance; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts new file mode 100644 index 000000000000..872639107987 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { IndexPattern } from '../../../../../data/public'; +import { useSelector } from '../../utils/state_management'; +import { DiscoverServices } from '../../../build_services'; + +export const useIndexPattern = (services: DiscoverServices) => { + const indexPatternId = useSelector((state) => state.metadata.indexPattern); + const [indexPattern, setIndexPattern] = useState(undefined); + const { data, toastNotifications } = services; + + useEffect(() => { + let isMounted = true; + if (!indexPatternId) return; + const indexPatternMissingWarning = i18n.translate( + 'discover.valueIsNotConfiguredIndexPatternIDWarningTitle', + { + defaultMessage: '{id} is not a configured index pattern ID', + values: { + id: `"${indexPatternId}"`, + }, + } + ); + + data.indexPatterns + .get(indexPatternId) + .then((result) => { + if (isMounted) { + setIndexPattern(result); + } + }) + .catch(() => { + if (isMounted) { + toastNotifications.addDanger({ + title: indexPatternMissingWarning, + }); + } + }); + + return () => { + isMounted = false; + }; + }, [indexPatternId, data.indexPatterns, toastNotifications]); + + return indexPattern; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts new file mode 100644 index 000000000000..50ad55bf4048 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -0,0 +1,287 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useMemo, useRef, useState } from 'react'; +import { BehaviorSubject, Subject, merge } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { i18n } from '@osd/i18n'; +import { useEffect } from 'react'; +import { RequestAdapter } from '../../../../../inspector/public'; +import { DiscoverServices } from '../../../build_services'; +import { search } from '../../../../../data/public'; +import { validateTimeRange } from '../../helpers/validate_time_range'; +import { updateSearchSource } from './update_search_source'; +import { useIndexPattern } from './use_index_pattern'; +import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; +import { TimechartHeaderBucketInterval } from '../../components/chart/timechart_header'; +import { tabifyAggResponse } from '../../../opensearch_dashboards_services'; +import { + getDimensions, + buildPointSeriesData, + createHistogramConfigs, + Chart, +} from '../../components/chart/utils'; +import { SavedSearch } from '../../../saved_searches'; +import { useSelector } from '../../utils/state_management'; +import { + getRequestInspectorStats, + getResponseInspectorStats, +} from '../../../opensearch_dashboards_services'; +import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../common'; +import { SortOrder } from '../../../saved_searches/types'; + +export enum ResultStatus { + UNINITIALIZED = 'uninitialized', + LOADING = 'loading', // initial data load + READY = 'ready', // results came back + NO_RESULTS = 'none', // no results came back +} + +export interface SearchData { + status: ResultStatus; + fetchCounter?: number; + fieldCounts?: Record; + hits?: number; + rows?: OpenSearchSearchHit[]; + bucketInterval?: TimechartHeaderBucketInterval | {}; + chartData?: Chart; +} + +export type SearchRefetch = 'refetch' | undefined; + +export type DataSubject = BehaviorSubject; +export type RefetchSubject = Subject; + +/** + * A hook that provides functionality for fetching and managing discover search data. + * @returns { data: DataSubject, refetch$: RefetchSubject, indexPattern: IndexPattern, savedSearch?: SavedSearch, inspectorAdapters } - data is a BehaviorSubject that emits the current search data, refetch$ is a Subject that can be used to trigger a refetch, savedSearch is the saved search object if it exists + * @example + * const { data$, refetch$ } = useSearch(); + * useEffect(() => { + * const subscription = data$.subscribe((d) => { + * // do something with the data + * }); + * return () => subscription.unsubscribe(); + * }, [data$]); + */ +export const useSearch = (services: DiscoverServices) => { + const [savedSearch, setSavedSearch] = useState(undefined); + const { savedSearch: savedSearchId, sort } = useSelector((state) => state.discover); + const indexPattern = useIndexPattern(services); + const { data, filterManager, getSavedSearchById, core, toastNotifications } = services; + const timefilter = data.query.timefilter.timefilter; + const fetchStateRef = useRef<{ + abortController: AbortController | undefined; + fieldCounts: Record; + rows?: OpenSearchSearchHit[]; + }>({ + abortController: undefined, + fieldCounts: {}, + }); + const inspectorAdapters = { + requests: new RequestAdapter(), + }; + + const shouldSearchOnPageLoad = useCallback(() => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + return ( + services.uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || + savedSearch?.id !== undefined || + timefilter.getRefreshInterval().pause === false + ); + }, [savedSearch, services.uiSettings, timefilter]); + + const data$ = useMemo( + () => + new BehaviorSubject({ + status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, + }), + [shouldSearchOnPageLoad] + ); + const refetch$ = useMemo(() => new Subject(), []); + const sort$ = useMemo(() => new Subject(), []); + + const fetch = useCallback( + async (sortArr: SortOrder[]) => { + if (!indexPattern) { + data$.next({ + status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, + }); + return; + } + + if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { + return data$.next({ + status: ResultStatus.NO_RESULTS, + rows: [], + }); + } + + // Abort any in-progress requests before fetching again + if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); + fetchStateRef.current.abortController = new AbortController(); + const histogramConfigs = indexPattern.timeFieldName + ? createHistogramConfigs(indexPattern, 'auto', data) + : undefined; + const searchSource = await updateSearchSource({ + indexPattern, + services, + sort: sortArr, + searchSource: savedSearch?.searchSource, + histogramConfigs, + }); + + try { + // Only show loading indicator if we are fetching when the rows are empty + if (fetchStateRef.current.rows?.length === 0) { + data$.next({ status: ResultStatus.LOADING }); + } + + // Initialize inspect adapter for search source + inspectorAdapters.requests.reset(); + const title = i18n.translate('discover.inspectorRequestDataTitle', { + defaultMessage: 'data', + }); + const description = i18n.translate('discover.inspectorRequestDescription', { + defaultMessage: 'This request queries OpenSearch to fetch the data for the search.', + }); + const inspectorRequest = inspectorAdapters.requests.start(title, { description }); + inspectorRequest.stats(getRequestInspectorStats(searchSource)); + searchSource.getSearchRequestBody().then((body) => { + inspectorRequest.json(body); + }); + + // Execute the search + const fetchResp = await searchSource.fetch({ + abortSignal: fetchStateRef.current.abortController.signal, + }); + + inspectorRequest + .stats(getResponseInspectorStats(fetchResp, searchSource)) + .ok({ json: fetchResp }); + const hits = fetchResp.hits.total as number; + const rows = fetchResp.hits.hits; + let bucketInterval = {}; + let chartData; + for (const row of rows) { + const fields = Object.keys(indexPattern.flattenHit(row)); + for (const fieldName of fields) { + fetchStateRef.current.fieldCounts[fieldName] = + (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1; + } + } + + if (histogramConfigs) { + const bucketAggConfig = histogramConfigs.aggs[1]; + const tabifiedData = tabifyAggResponse(histogramConfigs, fetchResp); + const dimensions = getDimensions(histogramConfigs, data); + if (dimensions) { + if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) { + bucketInterval = bucketAggConfig.buckets?.getInterval(); + } + // @ts-ignore tabifiedData is compatible but due to the way it is typed typescript complains + chartData = buildPointSeriesData(tabifiedData, dimensions); + } + } + + fetchStateRef.current.fieldCounts = fetchStateRef.current.fieldCounts!; + fetchStateRef.current.rows = rows; + data$.next({ + status: rows.length > 0 ? ResultStatus.READY : ResultStatus.NO_RESULTS, + fieldCounts: fetchStateRef.current.fieldCounts, + hits, + rows, + bucketInterval, + chartData, + }); + } catch (error) { + // If the request was aborted then no need to surface this error in the UI + if (error instanceof Error && error.name === 'AbortError') return; + + data$.next({ + status: ResultStatus.NO_RESULTS, + rows: [], + }); + + data.search.showError(error as Error); + } + }, + [ + indexPattern, + timefilter, + toastNotifications, + data, + services, + savedSearch?.searchSource, + data$, + shouldSearchOnPageLoad, + inspectorAdapters.requests, + ] + ); + + useEffect(() => { + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getTimeUpdate$(), + timefilter.getAutoRefreshFetch$(), + data.query.queryString.getUpdates$(), + sort$ + ).pipe(debounceTime(100)); + + const subscription = fetch$.subscribe(() => { + (async () => { + try { + await fetch(sort); + } catch (error) { + core.fatalErrors.add(error as Error); + } + })(); + }); + + // kick off initial fetch + refetch$.next(); + + return () => { + subscription.unsubscribe(); + }; + }, [ + data$, + data.query.queryString, + filterManager, + refetch$, + sort, + sort$, + timefilter, + fetch, + core.fatalErrors, + ]); + + // Get savedSearch if it exists + useEffect(() => { + (async () => { + const savedSearchInstance = await getSavedSearchById(savedSearchId || ''); + setSavedSearch(savedSearchInstance); + })(); + + return () => {}; + }, [getSavedSearchById, savedSearchId]); + + useEffect(() => { + sort$.next(sort); + }, [sort, sort$]); + + return { + data$, + refetch$, + indexPattern, + savedSearch, + inspectorAdapters, + }; +}; + +export type SearchContextValue = ReturnType; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 3fdafcff0c40..ebe4e80a70c5 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -57,6 +57,7 @@ import { getHistory } from './opensearch_dashboards_services'; import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; +import { DataExplorerServices } from '../../data_explorer/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -79,17 +80,15 @@ export interface DiscoverServices { toastNotifications: ToastsStart; getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; - getEmbeddableInjector: any; uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; } -export async function buildServices( +export function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, - context: PluginInitializerContext, - getEmbeddableInjector: any -): Promise { + context: PluginInitializerContext +): DiscoverServices { const services: SavedObjectOpenSearchDashboardsServices = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, @@ -108,7 +107,6 @@ export async function buildServices( docLinks: core.docLinks, theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, - getEmbeddableInjector, getSavedSearchById: async (id: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, @@ -127,3 +125,6 @@ export async function buildServices( visualizations: plugins.visualizations, }; } + +// Any component inside the panel and canvas views has access to both these services. +export type DiscoverViewServices = DiscoverServices & DataExplorerServices; diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 6c9ab46b656e..3bc009914940 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -37,5 +37,7 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; -export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; +// TODO: Fix embeddable after removing Angular +// export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; +export { NEW_DISCOVER_APP } from '../common'; diff --git a/src/plugins/discover/public/migrate_state.ts b/src/plugins/discover/public/migrate_state.ts new file mode 100644 index 000000000000..ef70a7ddfb67 --- /dev/null +++ b/src/plugins/discover/public/migrate_state.ts @@ -0,0 +1,138 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { matchPath } from 'react-router-dom'; +import { getStateFromOsdUrl, setStateToOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { Filter, Query } from '../../data/public'; + +interface CommonParams { + appState?: string; +} + +interface DiscoverParams extends CommonParams { + id?: string; +} + +interface ContextParams extends CommonParams { + indexPattern: string; + id: string; +} + +interface DocParams extends CommonParams { + indexPattern: string; + index: string; +} + +export interface LegacyDiscoverState { + /** + * Columns displayed in the table + */ + columns?: string[]; + /** + * Array of applied filters + */ + filters?: Filter[]; + /** + * id of the used index pattern + */ + index?: string; + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Lucence or DQL query + */ + query?: Query; + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; +} + +// TODO: Write unit tests once all routes have been migrated. +/** + * Migrates legacy URLs to the current URL format. + * @param oldPath The legacy hash that contains the state. + * @param newPath The new base path. + */ +export function migrateUrlState(oldPath: string, newPath = '/'): string { + let path = newPath; + const pathPatterns = [ + { + pattern: '#/context/:indexPattern/:id\\?:appState?', + extraState: { docView: 'context' }, + path: `context`, + }, + { + pattern: '#/doc/:indexPattern/:index\\?:appState?', + extraState: { docView: 'doc' }, + path: `doc`, + }, + { + pattern: '#/view/:id', + extraState: {}, + path: `savedSearch`, + }, + { pattern: '#/', extraState: {}, path: `discover` }, + ]; + + // Get the first matching path pattern. + const matchingPathPattern = pathPatterns.find((pathPattern) => + matchPath(oldPath, { path: pathPattern.pattern, strict: false }) + ); + + if (!matchingPathPattern) { + return path; + } + + // Migrate the path. + switch (matchingPathPattern.path) { + case `discover`: + case `savedSearch`: + const params = matchPath(oldPath, { + path: matchingPathPattern.pattern, + })!.params; + + // if there is a saved search id, use the saved search path + if (params.id) { + path = `${path}#/view/${params.id}`; + } + + const appState = getStateFromOsdUrl('_a', oldPath); + + if (!appState) return path; + + const { columns, filters, index, interval, query, sort, savedQuery } = appState; + + const _q = { + query, + filters, + }; + + const _a = { + discover: { + columns, + interval, + sort, + savedQuery, + }, + metadata: { + indexPattern: index, + }, + }; + + path = setStateToOsdUrl('_a', _a, { useHash: false }, path); + path = setStateToOsdUrl('_q', _q, { useHash: false }, path); + + break; + } + + return path; +} diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts index 8531564e0cc7..7149454a34ce 100644 --- a/src/plugins/discover/public/opensearch_dashboards_services.ts +++ b/src/plugins/discover/public/opensearch_dashboards_services.ts @@ -38,24 +38,9 @@ import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; -let angularModule: any = null; let services: DiscoverServices | null = null; let uiActions: UiActionsStart; -/** - * set bootstrapped inner angular module - */ -export function setAngularModule(module: any) { - angularModule = module; -} - -/** - * get boostrapped inner angular module - */ -export function getAngularModule() { - return angularModule; -} - export function getServices(): DiscoverServices { if (!services) { throw new Error('Discover services are not yet available'); @@ -74,11 +59,6 @@ export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGe AppMountParameters['setHeaderActionMenu'] >('headerActionMenuMounter'); -export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ - setTrackedUrl: (url: string) => void; - restorePreviousUrl: () => void; -}>('urlTracker'); - export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( 'DocViewsRegistry' ); @@ -86,6 +66,7 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter('DocViewsLinksRegistry'); + /** * Makes sure discover and context are using one instance of history. */ diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 62f6e6908ba1..16774500d00c 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -1,37 +1,10 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ import { i18n } from '@osd/i18n'; -import angular, { auto } from 'angular'; import { BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; import { AppMountParameters, @@ -56,9 +29,10 @@ import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { stringify } from 'query-string'; import rison from 'rison-node'; +import { lazy } from 'react'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; -import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; +import { url } from '../../opensearch_dashboards_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; @@ -70,25 +44,30 @@ import { JsonCodeBlock } from './application/components/json_code_block/json_cod import { setDocViewsRegistry, setDocViewsLinksRegistry, - setUrlTracker, - setAngularModule, setServices, setHeaderActionMenuMounter, setUiActions, setScopedHistory, - getScopedHistory, syncHistoryLocations, getServices, } from './opensearch_dashboards_services'; import { createSavedSearchesLoader } from './saved_searches'; -import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; import { DiscoverUrlGeneratorState, DISCOVER_APP_URL_GENERATOR, DiscoverUrlGenerator, } from './url_generator'; -import { SearchEmbeddableFactory } from './application/embeddable'; +// import { SearchEmbeddableFactory } from './application/embeddable'; +import { NEW_DISCOVER_APP, PLUGIN_ID } from '../common'; +import { DataExplorerPluginSetup } from '../../data_explorer/public'; +import { registerFeature } from './register_feature'; +import { + DiscoverState, + discoverSlice, + getPreloadedState, +} from './application/utils/state_management/discover_slice'; +import { migrateUrlState } from './migrate_state'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -103,7 +82,6 @@ export interface DiscoverSetup { docViews: { /** * Add new doc view shown along with table view and json view in the details of each document in Discover. - * Both react and angular doc views are supported. * @param docViewRaw */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; @@ -148,6 +126,7 @@ export interface DiscoverSetupPlugins { home?: HomePublicPluginSetup; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; + dataExplorer: DataExplorerPluginSetup; } /** @@ -166,13 +145,9 @@ export interface DiscoverStartPlugins { visualizations: VisualizationsStart; } -const innerAngularName = 'app/discover'; -const embeddableAngularName = 'app/discoverEmbeddable'; - /** * Contains Discover, one of the oldest parts of OpenSearch Dashboards - * There are 2 kinds of Angular bootstrapped for rendering, additionally to the main Angular - * Discover provides embeddables, those contain a slimmer Angular + * Discover provides embeddables for Dashboards */ export class DiscoverPlugin implements Plugin { @@ -181,18 +156,10 @@ export class DiscoverPlugin private appStateUpdater = new BehaviorSubject(() => ({})); private docViewsRegistry: DocViewsRegistry | null = null; private docViewsLinksRegistry: DocViewsLinksRegistry | null = null; - private embeddableInjector: auto.IInjectorService | null = null; private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; - private innerAngularInitialized: boolean = false; private urlGenerator?: DiscoverStart['urlGenerator']; - - /** - * why are those functions public? they are needed for some mocha tests - * can be removed once all is Jest - */ - public initializeInnerAngular?: () => void; - public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; + private initializeServices?: () => { core: CoreStart; plugins: DiscoverStartPlugins }; setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { const baseUrl = core.http.basePath.prepend('/app/discover'); @@ -270,45 +237,8 @@ export class DiscoverPlugin order: 2, }); - const { - appMounted, - appUnMounted, - stop: stopUrlTracker, - setActiveUrl: setTrackedUrl, - restorePreviousUrl, - } = createOsdUrlTracker({ - // we pass getter here instead of plain `history`, - // so history is lazily created (when app is mounted) - // this prevents redundant `#` when not in discover app - getHistory: getScopedHistory, - baseUrl, - defaultSubUrl: '#/', - storageKey: `lastUrl:${core.http.basePath.get()}:discover`, - navLinkUpdater$: this.appStateUpdater, - toastNotifications: core.notifications.toasts, - stateParams: [ - { - osdUrlKey: '_g', - stateUpdate$: plugins.data.query.state$.pipe( - filter( - ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(opensearchFilters.isFilterPinned), - })) - ), - }, - ], - }); - setUrlTracker({ setTrackedUrl, restorePreviousUrl }); - this.stopUrlTracking = () => { - stopUrlTracker(); - }; - - this.docViewsRegistry.setAngularInjectorGetter(this.getEmbeddableInjector); core.application.register({ - id: 'discover', + id: PLUGIN_ID, title: 'Discover', updater$: this.appStateUpdater.asObservable(), order: 1000, @@ -319,58 +249,91 @@ export class DiscoverPlugin if (!this.initializeServices) { throw Error('Discover plugin method initializeServices is undefined'); } - if (!this.initializeInnerAngular) { - throw Error('Discover plugin method initializeInnerAngular is undefined'); - } setScopedHistory(params.history); setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); - appMounted(); const { - plugins: { data: dataStart }, + core: { + application: { navigateToApp }, + }, } = await this.initializeServices(); - await this.initializeInnerAngular(); - - // make sure the index pattern list is up to date - await dataStart.indexPatterns.clearCache(); - const { renderApp } = await import('./application/application'); - params.element.classList.add('dscAppWrapper'); - const unmount = await renderApp(innerAngularName, params.element); - return () => { - params.element.classList.remove('dscAppWrapper'); - unmount(); - appUnMounted(); - }; + + // This is for instances where the user navigates to the app from the application nav menu + const path = window.location.hash; + const v2Enabled = await core.uiSettings.get(NEW_DISCOVER_APP); + if (!v2Enabled) { + navigateToApp('discoverLegacy', { + replace: true, + path, + }); + } else { + const newPath = migrateUrlState(path); + navigateToApp('data-explorer', { + replace: true, + path: `/${PLUGIN_ID}${newPath}`, + }); + } + + return () => {}; }, }); - plugins.urlForwarding.forwardApp('doc', 'discover', (path) => { - return `#${path}`; - }); - plugins.urlForwarding.forwardApp('context', 'discover', (path) => { - const urlParts = path.split('/'); - // take care of urls containing legacy url, those split in the following way - // ["", "context", indexPatternId, _type, id + params] - if (urlParts[4]) { - // remove _type part - const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/'); - return `#${newPath}`; - } - return `#${path}`; - }); - plugins.urlForwarding.forwardApp('discover', 'discover', (path) => { - const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || []; - if (!id) { - return `#${path.replace('/discover', '') || '/'}`; - } - return `#/view/${id}${tail || ''}`; - }); + // TODO: These routes need to be handled for Discover 2.0 to support legacy saved URLS's + // plugins.urlForwarding.forwardApp('doc', 'discover', (path) => { + // return `#${path}`; + // }); + // plugins.urlForwarding.forwardApp('context', 'discover', (path) => { + // const urlParts = path.split('/'); + // // take care of urls containing legacy url, those split in the following way + // // ["", "context", indexPatternId, _type, id + params] + // if (urlParts[4]) { + // // remove _type part + // const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/'); + // return `#${newPath}`; + // } + // return `#${path}`; + // }); + // plugins.urlForwarding.forwardApp('discover', 'discover', (path) => { + // const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || []; + // if (!id) { + // return `#${path.replace('/discover', '') || '/'}`; + // } + // return `#/view/${id}${tail || ''}`; + // }); if (plugins.home) { registerFeature(plugins.home); } - this.registerEmbeddable(core, plugins); + plugins.dataExplorer.registerView({ + id: PLUGIN_ID, + title: 'Discover', + defaultPath: '#/', + appExtentions: { + savedObject: { + docTypes: ['search'], + toListItem: (obj) => ({ + id: obj.id, + label: obj.title, + }), + }, + }, + ui: { + defaults: async () => { + this.initializeServices?.(); + const services = getServices(); + return await getPreloadedState(services); + }, + slice: discoverSlice, + }, + shouldShow: () => true, + // ViewComponent + Canvas: lazy(() => import('./application/view_components/canvas')), + Panel: lazy(() => import('./application/view_components/panel')), + Context: lazy(() => import('./application/view_components/context')), + }); + + // this.registerEmbeddable(core, plugins); return { docViews: { @@ -383,44 +346,21 @@ export class DiscoverPlugin } start(core: CoreStart, plugins: DiscoverStartPlugins) { - // we need to register the application service at setup, but to render it - // there are some start dependencies necessary, for this reason - // initializeInnerAngular + initializeServices are assigned at start and used - // when the application/embeddable is mounted - this.initializeInnerAngular = async () => { - if (this.innerAngularInitialized) { - return; - } - // this is used by application mount and tests - const { getInnerAngularModule } = await import('./get_inner_angular'); - const module = getInnerAngularModule( - innerAngularName, - core, - plugins, - this.initializerContext - ); - setAngularModule(module); - this.innerAngularInitialized = true; - }; - setUiActions(plugins.uiActions); - this.initializeServices = async () => { + this.initializeServices = () => { if (this.servicesInitialized) { return { core, plugins }; } - const services = await buildServices( - core, - plugins, - this.initializerContext, - this.getEmbeddableInjector - ); + const services = buildServices(core, plugins, this.initializerContext); setServices(services); this.servicesInitialized = true; return { core, plugins }; }; + this.initializeServices(); + return { urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ @@ -439,14 +379,11 @@ export class DiscoverPlugin } } + // TODO: Use this registration when legacy discover is removed /** * register embeddable with a slimmer embeddable version of inner angular */ private registerEmbeddable(core: CoreSetup, plugins: DiscoverSetupPlugins) { - if (!this.getEmbeddableInjector) { - throw Error('Discover plugin method getEmbeddableInjector is undefined'); - } - const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); return { @@ -455,23 +392,8 @@ export class DiscoverPlugin }; }; - const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); - plugins.embeddable.registerEmbeddableFactory(factory.type, factory); + // TODO: Refactor to remove angular + // const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); + // plugins.embeddable.registerEmbeddableFactory(factory.type, factory); } - - private getEmbeddableInjector = async () => { - if (!this.embeddableInjector) { - if (!this.initializeServices) { - throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); - } - const { core, plugins } = await this.initializeServices(); - getServices().opensearchDashboardsLegacy.loadFontAwesome(); - const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular'); - getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins); - const mountpoint = document.createElement('div'); - this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]); - } - - return this.embeddableInjector; - }; } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 55cd59104ecb..9b43e3a89203 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -34,6 +34,8 @@ import { SavedObjectOpenSearchDashboardsServices, } from '../../../saved_objects/public'; +export const SAVED_OBJECT_TYPE = 'search'; + export function createSavedSearchClass(services: SavedObjectOpenSearchDashboardsServices) { const SavedObjectClass = createSavedObjectClass(services); diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index e02fd65e6899..112d7d998c97 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -28,18 +28,16 @@ * under the License. */ +import { SavedObject } from '../../../saved_objects/public'; import { ISearchSource } from '../../../data/public'; -export type SortOrder = [string, string]; -export interface SavedSearch { - readonly id: string; - title: string; - searchSource: ISearchSource; +export type SortOrder = [string, 'asc' | 'desc']; +export interface SavedSearch + extends Pick { + searchSource: ISearchSource; // This is optional in SavedObject, but required for SavedSearch description?: string; columns: string[]; sort: SortOrder[]; - destroy: () => void; - lastSavedTitle?: string; } export interface SavedSearchLoader { get: (id: string) => Promise; diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 70eab306e7fb..2b35384c2e5c 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -33,6 +33,7 @@ import { schema } from '@osd/config-schema'; import { UiSettingsParams } from 'opensearch-dashboards/server'; import { + NEW_DISCOVER_APP, DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, AGGS_TERMS_SIZE_SETTING, @@ -47,6 +48,17 @@ import { } from '../common'; export const uiSettings: Record = { + [NEW_DISCOVER_APP]: { + name: i18n.translate('discover.advancedSettings.legacyToggleTitle', { + defaultMessage: 'Enable new discover app', + }), + value: true, + description: i18n.translate('discover.advancedSettings.legacyToggleText', { + defaultMessage: 'Disabling the new discover app will redirect to the legacy app.', + }), + category: ['discover'], + schema: schema.boolean(), + }, [DEFAULT_COLUMNS_SETTING]: { name: i18n.translate('discover.advancedSettings.defaultColumnsTitle', { defaultMessage: 'Default columns', diff --git a/src/plugins/discover_legacy/README.md b/src/plugins/discover_legacy/README.md new file mode 100644 index 000000000000..a914d651eef3 --- /dev/null +++ b/src/plugins/discover_legacy/README.md @@ -0,0 +1 @@ +Contains the Discover application and the saved search embeddable. \ No newline at end of file diff --git a/src/plugins/discover_legacy/common/index.ts b/src/plugins/discover_legacy/common/index.ts new file mode 100644 index 000000000000..371442385bbf --- /dev/null +++ b/src/plugins/discover_legacy/common/index.ts @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; +export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; +export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; +export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder'; +export const SEARCH_ON_PAGE_LOAD_SETTING = 'discover:searchOnPageLoad'; +export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn'; +export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; +export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; +export const CONTEXT_STEP_SETTING = 'context:step'; +export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; +export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; diff --git a/src/plugins/discover_legacy/opensearch_dashboards.json b/src/plugins/discover_legacy/opensearch_dashboards.json new file mode 100644 index 000000000000..6a4259a41d75 --- /dev/null +++ b/src/plugins/discover_legacy/opensearch_dashboards.json @@ -0,0 +1,27 @@ +{ + "id": "discoverLegacy", + "version": "opensearchDashboards", + "server": false, + "ui": true, + "requiredPlugins": [ + "charts", + "data", + "embeddable", + "inspector", + "opensearchDashboardsLegacy", + "urlForwarding", + "navigation", + "uiActions", + "visualizations" + ], + "optionalPlugins": [ + "home", + "share" + ], + "requiredBundles": [ + "opensearchDashboardsUtils", + "savedObjects", + "opensearchDashboardsReact", + "discover" + ] +} \ No newline at end of file diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover_legacy/public/application/_discover.scss similarity index 100% rename from src/plugins/discover/public/application/_discover.scss rename to src/plugins/discover_legacy/public/application/_discover.scss diff --git a/src/plugins/discover/public/application/angular/_index.scss b/src/plugins/discover_legacy/public/application/angular/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/_index.scss rename to src/plugins/discover_legacy/public/application/angular/_index.scss diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover_legacy/public/application/angular/context.html similarity index 100% rename from src/plugins/discover/public/application/angular/context.html rename to src/plugins/discover_legacy/public/application/angular/context.html diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover_legacy/public/application/angular/context.js similarity index 100% rename from src/plugins/discover/public/application/angular/context.js rename to src/plugins/discover_legacy/public/application/angular/context.js diff --git a/src/plugins/discover/public/application/angular/context/NOTES.md b/src/plugins/discover_legacy/public/application/angular/context/NOTES.md similarity index 100% rename from src/plugins/discover/public/application/angular/context/NOTES.md rename to src/plugins/discover_legacy/public/application/angular/context/NOTES.md diff --git a/src/plugins/discover/public/application/angular/context/_index.scss b/src/plugins/discover_legacy/public/application/angular/context/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/context/_index.scss rename to src/plugins/discover_legacy/public/application/angular/context/_index.scss diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.js b/src/plugins/discover_legacy/public/application/angular/context/api/_stubs.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/_stubs.js rename to src/plugins/discover_legacy/public/application/angular/context/api/_stubs.js diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.js b/src/plugins/discover_legacy/public/application/angular/context/api/anchor.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/anchor.js rename to src/plugins/discover_legacy/public/application/angular/context/api/anchor.js diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.js b/src/plugins/discover_legacy/public/application/angular/context/api/anchor.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/anchor.test.js rename to src/plugins/discover_legacy/public/application/angular/context/api/anchor.test.js diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js b/src/plugins/discover_legacy/public/application/angular/context/api/context.predecessors.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js rename to src/plugins/discover_legacy/public/application/angular/context/api/context.predecessors.test.js diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js b/src/plugins/discover_legacy/public/application/angular/context/api/context.successors.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/context.successors.test.js rename to src/plugins/discover_legacy/public/application/angular/context/api/context.successors.test.js diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover_legacy/public/application/angular/context/api/context.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/context.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/context.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.test.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/date_conversion.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/date_conversion.test.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/date_conversion.test.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/date_conversion.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/date_conversion.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/date_conversion.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/fetch_hits_in_interval.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/fetch_hits_in_interval.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/fetch_hits_in_interval.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/generate_intervals.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/generate_intervals.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_opensearch_query_search_after.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/get_opensearch_query_search_after.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/get_opensearch_query_search_after.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/get_opensearch_query_search_after.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_opensearch_query_sort.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/get_opensearch_query_sort.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/get_opensearch_query_sort.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/get_opensearch_query_sort.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.test.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/sorting.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/sorting.test.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/sorting.test.ts diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts b/src/plugins/discover_legacy/public/application/angular/context/api/utils/sorting.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/api/utils/sorting.ts rename to src/plugins/discover_legacy/public/application/angular/context/api/utils/sorting.ts diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/_action_bar.scss b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/_action_bar.scss similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/_action_bar.scss rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/_action_bar.scss diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/_index.scss b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/_index.scss rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/_index.scss diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar.test.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar.test.tsx diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar.tsx diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar_directive.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar_directive.ts diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar_warning.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/action_bar_warning.tsx diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/index.ts b/src/plugins/discover_legacy/public/application/angular/context/components/action_bar/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/components/action_bar/index.ts rename to src/plugins/discover_legacy/public/application/angular/context/components/action_bar/index.ts diff --git a/src/plugins/discover/public/application/angular/context/helpers/call_after_bindings_workaround.js b/src/plugins/discover_legacy/public/application/angular/context/helpers/call_after_bindings_workaround.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/helpers/call_after_bindings_workaround.js rename to src/plugins/discover_legacy/public/application/angular/context/helpers/call_after_bindings_workaround.js diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover_legacy/public/application/angular/context/query/actions.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/actions.js rename to src/plugins/discover_legacy/public/application/angular/context/query/actions.js diff --git a/src/plugins/discover/public/application/angular/context/query/constants.js b/src/plugins/discover_legacy/public/application/angular/context/query/constants.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/constants.js rename to src/plugins/discover_legacy/public/application/angular/context/query/constants.js diff --git a/src/plugins/discover/public/application/angular/context/query/index.js b/src/plugins/discover_legacy/public/application/angular/context/query/index.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/index.js rename to src/plugins/discover_legacy/public/application/angular/context/query/index.js diff --git a/src/plugins/discover/public/application/angular/context/query/state.js b/src/plugins/discover_legacy/public/application/angular/context/query/state.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/state.js rename to src/plugins/discover_legacy/public/application/angular/context/query/state.js diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.js b/src/plugins/discover_legacy/public/application/angular/context/query_parameters/actions.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/actions.js rename to src/plugins/discover_legacy/public/application/angular/context/query_parameters/actions.js diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover_legacy/public/application/angular/context/query_parameters/actions.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts rename to src/plugins/discover_legacy/public/application/angular/context/query_parameters/actions.test.ts diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/constants.ts b/src/plugins/discover_legacy/public/application/angular/context/query_parameters/constants.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/constants.ts rename to src/plugins/discover_legacy/public/application/angular/context/query_parameters/constants.ts diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/index.js b/src/plugins/discover_legacy/public/application/angular/context/query_parameters/index.js similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/index.js rename to src/plugins/discover_legacy/public/application/angular/context/query_parameters/index.js diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/state.ts b/src/plugins/discover_legacy/public/application/angular/context/query_parameters/state.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/state.ts rename to src/plugins/discover_legacy/public/application/angular/context/query_parameters/state.ts diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover_legacy/public/application/angular/context_app.html similarity index 100% rename from src/plugins/discover/public/application/angular/context_app.html rename to src/plugins/discover_legacy/public/application/angular/context_app.html diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover_legacy/public/application/angular/context_app.js similarity index 100% rename from src/plugins/discover/public/application/angular/context_app.js rename to src/plugins/discover_legacy/public/application/angular/context_app.js diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover_legacy/public/application/angular/context_state.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context_state.test.ts rename to src/plugins/discover_legacy/public/application/angular/context_state.test.ts diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover_legacy/public/application/angular/context_state.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context_state.ts rename to src/plugins/discover_legacy/public/application/angular/context_state.ts diff --git a/src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap b/src/plugins/discover_legacy/public/application/angular/directives/__snapshots__/no_results.test.js.snap similarity index 100% rename from src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap rename to src/plugins/discover_legacy/public/application/angular/directives/__snapshots__/no_results.test.js.snap diff --git a/src/plugins/discover/public/application/angular/directives/_histogram.scss b/src/plugins/discover_legacy/public/application/angular/directives/_histogram.scss similarity index 100% rename from src/plugins/discover/public/application/angular/directives/_histogram.scss rename to src/plugins/discover_legacy/public/application/angular/directives/_histogram.scss diff --git a/src/plugins/discover/public/application/angular/directives/_index.scss b/src/plugins/discover_legacy/public/application/angular/directives/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/directives/_index.scss rename to src/plugins/discover_legacy/public/application/angular/directives/_index.scss diff --git a/src/plugins/discover/public/application/angular/directives/_no_results.scss b/src/plugins/discover_legacy/public/application/angular/directives/_no_results.scss similarity index 100% rename from src/plugins/discover/public/application/angular/directives/_no_results.scss rename to src/plugins/discover_legacy/public/application/angular/directives/_no_results.scss diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.js b/src/plugins/discover_legacy/public/application/angular/directives/debounce/debounce.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/debounce/debounce.js rename to src/plugins/discover_legacy/public/application/angular/directives/debounce/debounce.js diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts b/src/plugins/discover_legacy/public/application/angular/directives/debounce/debounce.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts rename to src/plugins/discover_legacy/public/application/angular/directives/debounce/debounce.test.ts diff --git a/src/plugins/discover/public/application/angular/directives/debounce/index.js b/src/plugins/discover_legacy/public/application/angular/directives/debounce/index.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/debounce/index.js rename to src/plugins/discover_legacy/public/application/angular/directives/debounce/index.js diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.js b/src/plugins/discover_legacy/public/application/angular/directives/fixed_scroll.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/fixed_scroll.js rename to src/plugins/discover_legacy/public/application/angular/directives/fixed_scroll.js diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js b/src/plugins/discover_legacy/public/application/angular/directives/fixed_scroll.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js rename to src/plugins/discover_legacy/public/application/angular/directives/fixed_scroll.test.js diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover_legacy/public/application/angular/directives/histogram.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/directives/histogram.tsx rename to src/plugins/discover_legacy/public/application/angular/directives/histogram.tsx diff --git a/src/plugins/discover/public/application/angular/directives/index.js b/src/plugins/discover_legacy/public/application/angular/directives/index.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/index.js rename to src/plugins/discover_legacy/public/application/angular/directives/index.js diff --git a/src/plugins/discover/public/application/angular/directives/no_results.js b/src/plugins/discover_legacy/public/application/angular/directives/no_results.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/no_results.js rename to src/plugins/discover_legacy/public/application/angular/directives/no_results.js diff --git a/src/plugins/discover/public/application/angular/directives/no_results.test.js b/src/plugins/discover_legacy/public/application/angular/directives/no_results.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/directives/no_results.test.js rename to src/plugins/discover_legacy/public/application/angular/directives/no_results.test.js diff --git a/src/plugins/discover/public/application/angular/directives/render_complete.ts b/src/plugins/discover_legacy/public/application/angular/directives/render_complete.ts similarity index 100% rename from src/plugins/discover/public/application/angular/directives/render_complete.ts rename to src/plugins/discover_legacy/public/application/angular/directives/render_complete.ts diff --git a/src/plugins/discover_legacy/public/application/angular/directives/uninitialized.tsx b/src/plugins/discover_legacy/public/application/angular/directives/uninitialized.tsx new file mode 100644 index 000000000000..9cc47b034d1e --- /dev/null +++ b/src/plugins/discover_legacy/public/application/angular/directives/uninitialized.tsx @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; + +import { EuiButton, EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; + +interface Props { + onRefresh: () => void; +} + +export const DiscoverUninitialized = ({ onRefresh }: Props) => { + return ( + + + + + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> +
+
+
+
+ ); +}; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover_legacy/public/application/angular/discover.js similarity index 98% rename from src/plugins/discover/public/application/angular/discover.js rename to src/plugins/discover_legacy/public/application/angular/discover.js index de244e3c44b6..f8a96928784c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover_legacy/public/application/angular/discover.js @@ -93,6 +93,7 @@ import { DOC_HIDE_TIME_COLUMN_SETTING, MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; +import { NEW_DISCOVER_APP } from '../../../../discover/public'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -480,7 +481,24 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise }, }; + const newDiscover = { + id: 'discover-new', + label: i18n.translate('discover.localMenu.newDiscoverTitle', { + defaultMessage: 'New Discover', + }), + description: i18n.translate('discover.localMenu.newDiscoverDescription', { + defaultMessage: 'New Discover Experience', + }), + testId: 'discoverNewButton', + run: async function () { + await getServices().uiSettings.set(NEW_DISCOVER_APP, true); + window.location.reload(); + }, + type: 'toggle', + }; + return [ + newDiscover, newSearch, ...(uiCapabilities.discover.save ? [saveSearch] : []), openSearch, diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover_legacy/public/application/angular/discover_legacy.html similarity index 100% rename from src/plugins/discover/public/application/angular/discover_legacy.html rename to src/plugins/discover_legacy/public/application/angular/discover_legacy.html diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover_legacy/public/application/angular/discover_state.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/discover_state.test.ts rename to src/plugins/discover_legacy/public/application/angular/discover_state.test.ts diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover_legacy/public/application/angular/discover_state.ts similarity index 100% rename from src/plugins/discover/public/application/angular/discover_state.ts rename to src/plugins/discover_legacy/public/application/angular/discover_state.ts diff --git a/src/plugins/discover/public/application/angular/doc.html b/src/plugins/discover_legacy/public/application/angular/doc.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc.html rename to src/plugins/discover_legacy/public/application/angular/doc.html diff --git a/src/plugins/discover/public/application/angular/doc.ts b/src/plugins/discover_legacy/public/application/angular/doc.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc.ts rename to src/plugins/discover_legacy/public/application/angular/doc.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/_doc_table.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/_doc_table.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/_doc_table.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/actions/columns.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/actions/columns.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_index.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_index.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/_table_header.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/_table_header.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/index.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover_legacy/public/application/angular/doc_table/components/row_headers.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/row_headers.test.js diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/helpers.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/helpers.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header.test.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header.test.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header_column.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_header/table_header_column.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_cell.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_cell.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_details.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_details.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_open.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/_open.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/cell.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/cell.html diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/details.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/details.html diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/open.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/open.html diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.html b/src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/truncate_by_height.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.html rename to src/plugins/discover_legacy/public/application/angular/doc_table/components/table_row/truncate_by_height.html diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover_legacy/public/application/angular/doc_table/create_doc_table_react.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_table/create_doc_table_react.tsx diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.html similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/doc_table.html rename to src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.html diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.test.js similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/doc_table.test.js rename to src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.test.js diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/doc_table.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/doc_table.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js b/src/plugins/discover_legacy/public/application/angular/doc_table/doc_table_strings.js similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js rename to src/plugins/discover_legacy/public/application/angular/doc_table/doc_table_strings.js diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover_legacy/public/application/angular/doc_table/index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/index.scss rename to src/plugins/discover_legacy/public/application/angular/doc_table/index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/index.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/index.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/infinite_scroll.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/infinite_scroll.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_default_sort.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_default_sort.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort.test.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort.test.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort_for_search_source.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/get_sort_for_search_source.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/index.js similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/index.js diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/pager.js similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/pager.js diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/pager_factory.ts similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts rename to src/plugins/discover_legacy/public/application/angular/doc_table/lib/pager/pager_factory.ts diff --git a/src/plugins/discover/public/application/angular/doc_viewer.tsx b/src/plugins/discover_legacy/public/application/angular/doc_viewer.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_viewer.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_viewer.tsx diff --git a/src/plugins/discover/public/application/angular/doc_viewer_links.tsx b/src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_viewer_links.tsx rename to src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover_legacy/public/application/angular/helpers/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/helpers/index.ts rename to src/plugins/discover_legacy/public/application/angular/helpers/index.ts diff --git a/src/plugins/discover/public/application/angular/helpers/point_series.ts b/src/plugins/discover_legacy/public/application/angular/helpers/point_series.ts similarity index 100% rename from src/plugins/discover/public/application/angular/helpers/point_series.ts rename to src/plugins/discover_legacy/public/application/angular/helpers/point_series.ts diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover_legacy/public/application/angular/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/index.ts rename to src/plugins/discover_legacy/public/application/angular/index.ts diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover_legacy/public/application/angular/redirect.ts similarity index 100% rename from src/plugins/discover/public/application/angular/redirect.ts rename to src/plugins/discover_legacy/public/application/angular/redirect.ts diff --git a/src/plugins/discover/public/application/angular/response_handler.js b/src/plugins/discover_legacy/public/application/angular/response_handler.js similarity index 100% rename from src/plugins/discover/public/application/angular/response_handler.js rename to src/plugins/discover_legacy/public/application/angular/response_handler.js diff --git a/src/plugins/discover/public/application/application.ts b/src/plugins/discover_legacy/public/application/application.ts similarity index 100% rename from src/plugins/discover/public/application/application.ts rename to src/plugins/discover_legacy/public/application/application.ts diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx rename to src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message.test.tsx diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message.tsx similarity index 100% rename from src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx rename to src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message.tsx diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts b/src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message_directive.ts similarity index 100% rename from src/plugins/discover/public/application/components/context_error_message/context_error_message_directive.ts rename to src/plugins/discover_legacy/public/application/components/context_error_message/context_error_message_directive.ts diff --git a/src/plugins/discover/public/application/components/context_error_message/index.ts b/src/plugins/discover_legacy/public/application/components/context_error_message/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/context_error_message/index.ts rename to src/plugins/discover_legacy/public/application/components/context_error_message/index.ts diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover_legacy/public/application/components/create_discover_legacy_directive.ts similarity index 100% rename from src/plugins/discover/public/application/components/create_discover_legacy_directive.ts rename to src/plugins/discover_legacy/public/application/components/create_discover_legacy_directive.ts diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover_legacy/public/application/components/discover_legacy.tsx similarity index 95% rename from src/plugins/discover/public/application/components/discover_legacy.tsx rename to src/plugins/discover_legacy/public/application/components/discover_legacy.tsx index 3e6e96fc6124..d458e4339d3b 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover_legacy/public/application/components/discover_legacy.tsx @@ -166,21 +166,23 @@ export function DiscoverLegacy({

{savedSearch.title}

- +
+ +
{ + let registry: any[] = []; + + return { + getServices: () => ({ + metadata: { + branch: 'test', + }, + data: { + search: { + search: mockSearchApi, + }, + }, + }), + getDocViewsRegistry: () => ({ + addDocView(view: any) { + registry.push(view); + }, + getDocViewsSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), + getDocViewsLinksRegistry: () => ({ + addDocViewLink(view: any) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), + }; +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const waitForPromises = async () => + act(async () => { + await new Promise((resolve) => setTimeout(resolve)); + }); + +/** + * this works but logs ugly error messages until we're using React 16.9 + * should be adapted when we upgrade + */ +async function mountDoc(update = false, indexPatternGetter: any = null) { + const indexPattern = { + getComputedFields: () => [], + }; + const indexPatternService = { + get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)), + } as any; + + const props = { + id: '1', + index: 'index1', + indexPatternId: 'xyz', + indexPatternService, + } as DocProps; + let comp!: ReactWrapper; + await act(async () => { + comp = mountWithIntl(); + if (update) comp.update(); + }); + if (update) { + await waitForPromises(); + comp.update(); + } + return comp; +} + +describe('Test of of Discover', () => { + test('renders loading msg', async () => { + const comp = await mountDoc(); + expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1); + }); + + test('renders IndexPattern notFound msg', async () => { + const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' })); + const comp = await mountDoc(true, indexPatternGetter); + expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1); + }); + + test('renders notFound msg', async () => { + mockSearchApi.mockImplementation(() => throwError({ status: 404 })); + const comp = await mountDoc(true); + expect(findTestSubject(comp, 'doc-msg-notFound').length).toBe(1); + }); + + test('renders error msg', async () => { + mockSearchApi.mockImplementation(() => throwError({ error: 'something else' })); + const comp = await mountDoc(true); + expect(findTestSubject(comp, 'doc-msg-error').length).toBe(1); + }); + + test('renders opensearch hit ', async () => { + mockSearchApi.mockImplementation(() => + of({ rawResponse: { hits: { total: 1, hits: [{ _id: 1, _source: { test: 1 } }] } } }) + ); + const comp = await mountDoc(true); + expect(findTestSubject(comp, 'doc-hit').length).toBe(1); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/doc/doc.tsx b/src/plugins/discover_legacy/public/application/components/doc/doc.tsx new file mode 100644 index 000000000000..204a16d64757 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc/doc.tsx @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui'; +import { IndexPatternsContract } from 'src/plugins/data/public'; +import { OpenSearchRequestState, useOpenSearchDocSearch } from './use_opensearch_doc_search'; +import { DocViewer } from '../doc_viewer/doc_viewer'; + +export interface DocProps { + /** + * Id of the doc in OpenSearch + */ + id: string; + /** + * Index in OpenSearch to query + */ + index: string; + /** + * IndexPattern ID used to get IndexPattern entity + * that's used for adding additional fields (stored_fields, script_fields, docvalue_fields) + */ + indexPatternId: string; + /** + * IndexPatternService to get a given index pattern by ID + */ + indexPatternService: IndexPatternsContract; +} + +export function Doc(props: DocProps) { + const [reqState, hit, indexPattern] = useOpenSearchDocSearch(props); + return ( + + + {reqState === OpenSearchRequestState.NotFoundIndexPattern && ( + + } + /> + )} + {reqState === OpenSearchRequestState.NotFound && ( + + } + > + + + )} + + {reqState === OpenSearchRequestState.Error && ( + + } + > + {' '} + + + + + )} + + {reqState === OpenSearchRequestState.Loading && ( + + {' '} + + + )} + + {reqState === OpenSearchRequestState.Found && hit !== null && indexPattern && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx new file mode 100644 index 000000000000..cb716a4f17cb --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { + buildSearchBody, + useOpenSearchDocSearch, + OpenSearchRequestState, +} from './use_opensearch_doc_search'; +import { DocProps } from './doc'; +import { Observable } from 'rxjs'; + +const mockSearchResult = new Observable(); + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getServices: () => ({ + data: { + search: { + search: jest.fn(() => { + return mockSearchResult; + }), + }, + }, + }), +})); + +describe('Test of helper / hook', () => { + test('buildSearchBody', () => { + const indexPattern = { + getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), + } as any; + const actual = buildSearchBody('1', indexPattern); + expect(actual).toMatchInlineSnapshot(` + Object { + "_source": true, + "docvalue_fields": Array [], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, + }, + "script_fields": Array [], + "stored_fields": Array [], + } + `); + }); + + test('useOpenSearchDocSearch', async () => { + const indexPattern = { + getComputedFields: () => [], + }; + const indexPatternService = { + get: jest.fn(() => Promise.resolve(indexPattern)), + } as any; + const props = { + id: '1', + index: 'index1', + indexPatternId: 'xyz', + indexPatternService, + } as DocProps; + let hook; + await act(async () => { + hook = renderHook((p: DocProps) => useOpenSearchDocSearch(p), { initialProps: props }); + }); + // @ts-ignore + expect(hook.result.current).toEqual([OpenSearchRequestState.Loading, null, indexPattern]); + expect(indexPatternService.get).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts new file mode 100644 index 000000000000..b5ca9fec1c2f --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEffect, useState } from 'react'; +import { IndexPattern, getServices } from '../../../opensearch_dashboards_services'; +import { DocProps } from './doc'; +import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; + +export enum OpenSearchRequestState { + Loading, + NotFound, + Found, + Error, + NotFoundIndexPattern, +} + +/** + * helper function to build a query body for OpenSearch + * https://opensearch.org/docs/latest/opensearch/query-dsl/index/ + */ +export function buildSearchBody(id: string, indexPattern: IndexPattern): Record { + const computedFields = indexPattern.getComputedFields(); + + return { + query: { + ids: { + values: [id], + }, + }, + stored_fields: computedFields.storedFields, + _source: true, + script_fields: computedFields.scriptFields, + docvalue_fields: computedFields.docvalueFields, + }; +} + +/** + * Custom react hook for querying a single doc in OpenSearch + */ +export function useOpenSearchDocSearch({ + id, + index, + indexPatternId, + indexPatternService, +}: DocProps): [OpenSearchRequestState, OpenSearchSearchHit | null, IndexPattern | null] { + const [indexPattern, setIndexPattern] = useState(null); + const [status, setStatus] = useState(OpenSearchRequestState.Loading); + const [hit, setHit] = useState(null); + + useEffect(() => { + async function requestData() { + try { + const indexPatternEntity = await indexPatternService.get(indexPatternId); + setIndexPattern(indexPatternEntity); + + const { rawResponse } = await getServices() + .data.search.search({ + dataSourceId: indexPatternEntity.dataSourceRef?.id, + params: { + index, + body: buildSearchBody(id, indexPatternEntity), + }, + }) + .toPromise(); + + const hits = rawResponse.hits; + + if (hits?.hits?.[0]) { + setStatus(OpenSearchRequestState.Found); + setHit(hits.hits[0]); + } else { + setStatus(OpenSearchRequestState.NotFound); + } + } catch (err) { + if (err.savedObjectId) { + setStatus(OpenSearchRequestState.NotFoundIndexPattern); + } else if (err.status === 404) { + setStatus(OpenSearchRequestState.NotFound); + } else { + setStatus(OpenSearchRequestState.Error); + } + } + } + requestData(); + }, [id, index, indexPatternId, indexPatternService]); + return [status, hit, indexPattern]; +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap new file mode 100644 index 000000000000..cc1647fe264e --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render with 3 different tabs 1`] = ` +
+ , + "id": "osd_doc_viewer_tab_0", + "name": "Render function", + }, + Object { + "content": , + "id": "osd_doc_viewer_tab_1", + "name": "React component", + }, + Object { + "content": , + "id": "osd_doc_viewer_tab_2", + "name": "Invalid doc view", + }, + ] + } + /> +
+`; diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap new file mode 100644 index 000000000000..31509659ce41 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Mounting and unmounting DocViewerRenderTab 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ +
, + Object { + "hit": Object {}, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": [MockFunction], + }, + ], +} +`; diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss new file mode 100644 index 000000000000..91b66fc84297 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss @@ -0,0 +1,72 @@ +.osdDocViewerTable { + margin-top: $euiSizeS; +} + +.osdDocViewer { + pre, + .osdDocViewer__value { + display: inline-block; + word-break: break-all; + word-wrap: break-word; + white-space: pre-wrap; + color: $euiColorFullShade; + vertical-align: top; + padding-top: 2px; + } + + .osdDocViewer__field { + padding-top: 8px; + } + + .dscFieldName { + color: $euiColorDarkShade; + } + + td, + pre { + font-family: $euiCodeFontFamily; + } + + tr:first-child td { + border-top-color: transparent; + } + + tr:hover { + .osdDocViewer__actionButton { + opacity: 1; + } + } +} + +.osdDocViewer__buttons, +.osdDocViewer__field { + white-space: nowrap; +} + +.osdDocViewer__buttons { + width: 60px; + + // Show all icons if one is focused, + // IE doesn't support, but the fallback is just the focused button becomes visible + &:focus-within { + .osdDocViewer__actionButton { + opacity: 1; + } + } +} + +.osdDocViewer__field { + width: 160px; +} + +.osdDocViewer__actionButton { + opacity: 0; + + &:focus { + opacity: 1; + } +} + +.osdDocViewer__warning { + margin-right: $euiSizeS; +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx new file mode 100644 index 000000000000..ccab0be41ed2 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { DocViewer } from './doc_viewer'; +import { findTestSubject } from 'test_utils/helpers'; +import { getDocViewsRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewRenderProps } from '../../doc_views/doc_views_types'; + +jest.mock('../../../opensearch_dashboards_services', () => { + let registry: any[] = []; + return { + getDocViewsRegistry: () => ({ + addDocView(view: any) { + registry.push(view); + }, + getDocViewsSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), + }; +}); + +beforeEach(() => { + (getDocViewsRegistry() as any).resetRegistry(); + jest.clearAllMocks(); +}); + +test('Render with 3 different tabs', () => { + const registry = getDocViewsRegistry(); + registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() }); + registry.addDocView({ order: 20, title: 'React component', component: () =>
test
}); + registry.addDocView({ order: 30, title: 'Invalid doc view' }); + + const renderProps = { hit: {} } as DocViewRenderProps; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); +}); + +test('Render with 1 tab displaying error message', () => { + function SomeComponent() { + // this is just a placeholder + return null; + } + + const registry = getDocViewsRegistry(); + registry.addDocView({ + order: 10, + title: 'React component', + component: SomeComponent, + }); + + const renderProps = { hit: {} } as DocViewRenderProps; + const errorMsg = 'Catch me if you can!'; + + const wrapper = mount(); + const error = new Error(errorMsg); + wrapper.find(SomeComponent).simulateError(error); + const errorMsgComponent = findTestSubject(wrapper, 'docViewerError'); + expect(errorMsgComponent.text()).toMatch(new RegExp(`${errorMsg}`)); +}); diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx new file mode 100644 index 000000000000..d165c9bd05b8 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './doc_viewer.scss'; +import React from 'react'; +import { EuiTabbedContent } from '@elastic/eui'; +import { getDocViewsRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewerTab } from './doc_viewer_tab'; +import { DocView, DocViewRenderProps } from '../../doc_views/doc_views_types'; + +/** + * Rendering tabs with different views of 1 OpenSearch hit in Discover. + * The tabs are provided by the `docs_views` registry. + * A view can contain a React `component`, or any JS framework by using + * a `render` function. + */ +export function DocViewer(renderProps: DocViewRenderProps) { + const docViewsRegistry = getDocViewsRegistry(); + const tabs = docViewsRegistry + .getDocViewsSorted(renderProps.hit) + .map(({ title, render, component }: DocView, idx: number) => { + return { + id: `osd_doc_viewer_tab_${idx}`, + name: title, + content: ( + + ), + }; + }); + + if (!tabs.length) { + // There there's a minimum of 2 tabs active in Discover. + // This condition takes care of unit tests with 0 tabs. + return null; + } + + return ( +
+ +
+ ); +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx new file mode 100644 index 000000000000..1cb14d191a57 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; +import { formatMsg, formatStack } from '../../../../../opensearch_dashboards_legacy/public'; + +interface Props { + error: Error | string; +} + +export function DocViewerError({ error }: Props) { + const errMsg = formatMsg(error); + const errStack = typeof error === 'object' ? formatStack(error) : ''; + + return ( + + {errStack && {errStack}} + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx new file mode 100644 index 000000000000..83d857b24fc5 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { DocViewRenderTab } from './doc_viewer_render_tab'; +import { DocViewRenderProps } from '../../doc_views/doc_views_types'; + +test('Mounting and unmounting DocViewerRenderTab', () => { + const unmountFn = jest.fn(); + const renderFn = jest.fn(() => unmountFn); + const renderProps = { + hit: {}, + }; + + const wrapper = mount( + + ); + + expect(renderFn).toMatchSnapshot(); + + wrapper.unmount(); + + expect(unmountFn).toBeCalled(); +}); diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx new file mode 100644 index 000000000000..edc7f40c5e43 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useRef, useEffect } from 'react'; +import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; + +interface Props { + render: DocViewRenderFn; + renderProps: DocViewRenderProps; +} +/** + * Responsible for rendering a tab provided by a render function. + * So any other framework can be used (E.g. legacy Angular 3rd party plugin code) + * The provided `render` function is called with a reference to the + * component's `HTMLDivElement` as 1st arg and `renderProps` as 2nd arg + */ +export function DocViewRenderTab({ render, renderProps }: Props) { + const ref = useRef(null); + useEffect(() => { + if (ref && ref.current) { + return render(ref.current, renderProps); + } + }, [render, renderProps]); + return
; +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx new file mode 100644 index 000000000000..6e7a5f1ac434 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { DocViewRenderTab } from './doc_viewer_render_tab'; +import { DocViewerError } from './doc_viewer_render_error'; +import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; + +interface Props { + component?: React.ComponentType; + id: number; + render?: DocViewRenderFn; + renderProps: DocViewRenderProps; + title: string; +} + +interface State { + error: Error | string; + hasError: boolean; +} +/** + * Renders the tab content of a doc view. + * Displays an error message when it encounters exceptions, thanks to + * Error Boundaries. + */ +export class DocViewerTab extends React.Component { + state = { + hasError: false, + error: '', + }; + + static getDerivedStateFromError(error: unknown) { + // Update state so the next render will show the fallback UI. + return { hasError: true, error }; + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + return ( + nextProps.renderProps.hit._id !== this.props.renderProps.hit._id || + nextProps.id !== this.props.id || + nextState.hasError + ); + } + + render() { + const { component, render, renderProps, title } = this.props; + const { hasError, error } = this.state; + + if (hasError && error) { + return ; + } else if (!render && !component) { + return ( + + ); + } + + if (render) { + // doc view is provided by a render function, e.g. for legacy Angular code + return ; + } + + // doc view is provided by a react component + + const Component = component as any; + return ( + + + + ); + } +} diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap new file mode 100644 index 000000000000..95fb0c377180 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Dont Render if generateCb.hide 1`] = ` + +`; + +exports[`Render with 2 different links 1`] = ` + + + + + + + + +`; diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx new file mode 100644 index 000000000000..8aba555b3a37 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { DocViewerLinks } from './doc_viewer_links'; +import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types'; + +jest.mock('../../../opensearch_dashboards_services', () => { + let registry: any[] = []; + return { + getDocViewsLinksRegistry: () => ({ + addDocViewLink(view: any) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), + }; +}); + +beforeEach(() => { + (getDocViewsLinksRegistry() as any).resetRegistry(); + jest.clearAllMocks(); +}); + +test('Render with 2 different links', () => { + const registry = getDocViewsLinksRegistry(); + registry.addDocViewLink({ + order: 10, + label: 'generateCb link', + generateCb: () => ({ + url: 'aaa', + }), + }); + registry.addDocViewLink({ order: 20, label: 'href link', href: 'bbb' }); + + const renderProps = { hit: {} } as DocViewLinkRenderProps; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); +}); + +test('Dont Render if generateCb.hide', () => { + const registry = getDocViewsLinksRegistry(); + registry.addDocViewLink({ + order: 10, + label: 'generateCb link', + generateCb: () => ({ + url: 'aaa', + hide: true, + }), + }); + + const renderProps = { hit: {} } as DocViewLinkRenderProps; + + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx new file mode 100644 index 000000000000..9efb0693fde6 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiListGroupItem, EuiListGroupItemProps } from '@elastic/eui'; +import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services'; +import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types'; + +export function DocViewerLinks(renderProps: DocViewLinkRenderProps) { + const listItems = getDocViewsLinksRegistry() + .getDocViewsLinksSorted() + .filter((item) => !(item.generateCb && item.generateCb(renderProps)?.hide)) + .map((item) => { + const { generateCb, href, ...props } = item; + const listItem: EuiListGroupItemProps = { + 'data-test-subj': 'docTableRowAction', + ...props, + href: generateCb ? generateCb(renderProps).url : href, + }; + + return listItem; + }); + + return ( + + {listItems.map((item, index) => ( + + + + ))} + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap new file mode 100644 index 000000000000..cfd81a66acae --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` +
+
+ + + +
+
+ + + t.t.test + + +
+
+`; + +exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = ` +
+
+ + + +
+
+ + + test.test.test + + +
+
+`; + +exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = ` +
+
+ + + +
+
+ + + test + + +
+
+`; diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx new file mode 100644 index 000000000000..54dc902837d0 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render } from 'enzyme'; +import { FieldName } from './field_name'; + +// Note that it currently provides just 2 basic tests, there should be more, but +// the components involved will soon change +test('FieldName renders a string field by providing fieldType and fieldName', () => { + const component = render(); + expect(component).toMatchSnapshot(); +}); + +test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => { + const component = render(); + expect(component).toMatchSnapshot(); +}); + +test('FieldName renders a geo field, useShortDots is set to true', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); +}); diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx b/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx new file mode 100644 index 000000000000..bbd9ab79d0fb --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +import { FieldIcon, FieldIconProps } from '../../../../../opensearch_dashboards_react/public'; +import { shortenDottedString } from '../../helpers'; +import { getFieldTypeName } from './field_type_name'; + +// properties fieldType and fieldName are provided in osd_doc_view +// this should be changed when both components are deangularized +interface Props { + fieldName: string; + fieldType: string; + useShortDots?: boolean; + fieldIconProps?: Omit; + scripted?: boolean; +} + +export function FieldName({ + fieldName, + fieldType, + useShortDots, + fieldIconProps, + scripted = false, +}: Props) { + const typeName = getFieldTypeName(fieldType); + const displayName = useShortDots ? shortenDottedString(fieldName) : fieldName; + + return ( + + + + + + + {displayName} + + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts b/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts new file mode 100644 index 000000000000..38b18792d3e4 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; + +export function getFieldTypeName(type: string) { + switch (type) { + case 'boolean': + return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean field', + }); + case 'conflict': + return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflicting field', + }); + case 'date': + return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date field', + }); + case 'geo_point': + return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point field', + }); + case 'geo_shape': + return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape field', + }); + case 'ip': + return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address field', + }); + case 'murmur3': + return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3 field', + }); + case 'number': + return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number field', + }); + case 'source': + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + case 'string': + return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String field', + }); + case 'nested': + return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested field', + }); + default: + return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { + defaultMessage: 'Unknown field', + }); + } +} diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js b/src/plugins/discover_legacy/public/application/components/help_menu/help_menu_util.js similarity index 100% rename from src/plugins/discover/public/application/components/help_menu/help_menu_util.js rename to src/plugins/discover_legacy/public/application/components/help_menu/help_menu_util.js diff --git a/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx new file mode 100644 index 000000000000..998ababbc47f --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { HitsCounter, HitsCounterProps } from './hits_counter'; +import { findTestSubject } from 'test_utils/helpers'; + +describe('hits counter', function () { + let props: HitsCounterProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + onResetQuery: jest.fn(), + showResetButton: true, + hits: 2, + }; + }); + + it('HitsCounter renders a button by providing the showResetButton property', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); + }); + + it('HitsCounter not renders a button when the showResetButton property is false', () => { + component = mountWithIntl( + + ); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); + }); + + it('expect to render the number of hits', function () { + component = mountWithIntl(); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('2'); + }); + + it('expect to render 1,899 hits if 1899 hits given', function () { + component = mountWithIntl( + + ); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('1,899'); + }); + + it('should reset query', function () { + component = mountWithIntl(); + findTestSubject(component, 'resetSavedSearch').simulate('click'); + expect(props.onResetQuery).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.tsx similarity index 100% rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx rename to src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.tsx diff --git a/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts b/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts new file mode 100644 index 000000000000..213cf96e0cc8 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { HitsCounter } from './hits_counter'; diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap new file mode 100644 index 000000000000..3897e22c50f1 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`returns the \`JsonCodeEditor\` component 1`] = ` + + { + "_index": "test", + "_type": "doc", + "_id": "foo", + "_score": 1, + "_source": { + "test": 123 + } +} + +`; diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx new file mode 100644 index 000000000000..2cb700b4d2ac --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { JsonCodeBlock } from './json_code_block'; +import { IndexPattern } from '../../../../../data/public'; + +it('returns the `JsonCodeEditor` component', () => { + const props = { + hit: { _index: 'test', _type: 'doc', _id: 'foo', _score: 1, _source: { test: 123 } }, + columns: [], + indexPattern: {} as IndexPattern, + filter: jest.fn(), + onAddColumn: jest.fn(), + onRemoveColumn: jest.fn(), + }; + expect(shallow()).toMatchSnapshot(); +}); diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx new file mode 100644 index 000000000000..f33cae438cb2 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { DocViewRenderProps } from '../../doc_views/doc_views_types'; + +export function JsonCodeBlock({ hit }: DocViewRenderProps) { + const label = i18n.translate('discover.docViews.json.codeEditorAriaLabel', { + defaultMessage: 'Read only JSON view of an opensearch document', + }); + return ( + + {JSON.stringify(hit, null, 2)} + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx new file mode 100644 index 000000000000..fbc98e2550e0 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { LoadingSpinner } from './loading_spinner'; +import { findTestSubject } from 'test_utils/helpers'; + +describe('loading spinner', function () { + let component: ReactWrapper; + + it('LoadingSpinner renders a Searching text and a spinner', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'loadingSpinnerText').text()).toBe('Searching'); + expect(findTestSubject(component, 'loadingSpinner').length).toBe(1); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx new file mode 100644 index 000000000000..697c7a136d60 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +export function LoadingSpinner() { + return ( + <> + +

+ +

+
+ + + + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap rename to src/plugins/discover_legacy/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/change_indexpattern.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx rename to src/plugins/discover_legacy/public/application/components/sidebar/change_indexpattern.tsx diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss new file mode 100644 index 000000000000..8e1dd41f66ab --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss @@ -0,0 +1,4 @@ +.dscSidebarItem__fieldPopoverPanel { + min-width: 260px; + max-width: 300px; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx new file mode 100644 index 000000000000..1b384a4b5550 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DiscoverField } from './discover_field'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getServices: () => ({ + history: () => ({ + location: { + search: '', + }, + }), + capabilities: { + visualize: { + show: true, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } else if (key === 'shortDots:enable') { + return false; + } + }, + }, + }), +})); + +function getComponent({ + selected = false, + showDetails = false, + useShortDots = false, + field, +}: { + selected?: boolean; + showDetails?: boolean; + useShortDots?: boolean; + field?: IndexPatternField; +}) { + const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + + const finalField = + field ?? + new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + + const props = { + indexPattern, + columns: [], + field: finalField, + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 1 })), + onAddFilter: jest.fn(), + onAddField: jest.fn(), + onRemoveField: jest.fn(), + showDetails, + selected, + useShortDots, + }; + const comp = mountWithIntl(); + return { comp, props }; +} + +describe('discover sidebar field', function () { + it('should allow selecting fields', function () { + const { comp, props } = getComponent({}); + findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); + }); + it('should allow deselecting fields', function () { + const { comp, props } = getComponent({ selected: true }); + findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); + }); + it('should trigger getDetails', function () { + const { comp, props } = getComponent({ selected: true }); + findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + expect(props.getDetails).toHaveBeenCalledWith(props.field); + }); + it('should not allow clicking on _source', function () { + const field = new IndexPatternField( + { + name: '_source', + type: '_source', + esTypes: ['_source'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + '_source' + ); + const { comp, props } = getComponent({ + selected: true, + field, + }); + findTestSubject(comp, 'field-_source-showDetails').simulate('click'); + expect(props.getDetails).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx new file mode 100644 index 000000000000..e807267435eb --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx @@ -0,0 +1,245 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { DiscoverFieldDetails } from './discover_field_details'; +import { FieldIcon, FieldButton } from '../../../../../opensearch_dashboards_react/public'; +import { FieldDetails } from './types'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import { shortenDottedString } from '../../helpers'; +import { getFieldTypeName } from './lib/get_field_type_name'; +import './discover_field.scss'; + +export interface DiscoverFieldProps { + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; + /** + * The displayed field + */ + field: IndexPatternField; + /** + * The currently selected index pattern + */ + indexPattern: IndexPattern; + /** + * Callback to add/select the field + */ + onAddField: (fieldName: string) => void; + /** + * Callback to add a filter to filter bar + */ + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Callback to remove/deselect a the field + * @param fieldName + */ + onRemoveField: (fieldName: string) => void; + /** + * Retrieve details data for the field + */ + getDetails: (field: IndexPatternField) => FieldDetails; + /** + * Determines whether the field is selected + */ + selected?: boolean; + /** + * Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2 + */ + useShortDots?: boolean; +} + +export function DiscoverField({ + columns, + field, + indexPattern, + onAddField, + onRemoveField, + onAddFilter, + getDetails, + selected, + useShortDots, +}: DiscoverFieldProps) { + const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { + defaultMessage: 'Add {field} to table', + values: { field: field.name }, + }); + const removeLabelAria = i18n.translate( + 'discover.fieldChooser.discoverField.removeButtonAriaLabel', + { + defaultMessage: 'Remove {field} from table', + values: { field: field.name }, + } + ); + + const [infoIsOpen, setOpen] = useState(false); + + const toggleDisplay = (f: IndexPatternField) => { + if (selected) { + onRemoveField(f.name); + } else { + onAddField(f.name); + } + }; + + function togglePopover() { + setOpen(!infoIsOpen); + } + + function wrapOnDot(str?: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; + } + + const dscFieldIcon = ( + + ); + + const fieldName = ( + + {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)} + + ); + + let actionButton; + if (field.name !== '_source' && !selected) { + actionButton = ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(field); + }} + data-test-subj={`fieldToggle-${field.name}`} + aria-label={addLabelAria} + /> + + ); + } else if (field.name !== '_source' && selected) { + actionButton = ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(field); + }} + data-test-subj={`fieldToggle-${field.name}`} + aria-label={removeLabelAria} + /> + + ); + } + + if (field.type === '_source') { + return ( + + ); + } + + return ( + { + togglePopover(); + }} + dataTestSubj={`field-${field.name}-showDetails`} + fieldIcon={dscFieldIcon} + fieldAction={actionButton} + fieldName={fieldName} + /> + } + isOpen={infoIsOpen} + closePopover={() => setOpen(false)} + anchorPosition="rightUp" + panelClassName="dscSidebarItem__fieldPopoverPanel" + > + + {' '} + {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} + + {infoIsOpen && ( + + )} + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss new file mode 100644 index 000000000000..90b645f70084 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss @@ -0,0 +1,4 @@ +.dscFieldDetails__barContainer { + // Constrains value to the flex item, and allows for truncation when necessary + min-width: 0; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx new file mode 100644 index 000000000000..6a4dbe295e50 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx @@ -0,0 +1,133 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { StringFieldProgressBar } from './string_progress_bar'; +import { Bucket } from './types'; +import { IndexPatternField } from '../../../../../data/public'; +import './discover_field_bucket.scss'; + +interface Props { + bucket: Bucket; + field: IndexPatternField; + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} + +export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) { + const emptyTxt = i18n.translate('discover.fieldChooser.detailViews.emptyStringText', { + defaultMessage: 'Empty string', + }); + const addLabel = i18n.translate('discover.fieldChooser.detailViews.filterValueButtonAriaLabel', { + defaultMessage: 'Filter for {field}: "{value}"', + values: { value: bucket.value, field: field.name }, + }); + const removeLabel = i18n.translate( + 'discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel', + { + defaultMessage: 'Filter out {field}: "{value}"', + values: { value: bucket.value, field: field.name }, + } + ); + + return ( + <> + + + + + + {bucket.display === '' ? emptyTxt : bucket.display} + + + + + {bucket.percent.toFixed(1)}% + + + + + + {field.filterable && ( + +
+ onAddFilter(field, bucket.value, '+')} + aria-label={addLabel} + data-test-subj={`plus-${field.name}-${bucket.value}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + className={'euiButtonIcon--auto'} + /> + onAddFilter(field, bucket.value, '-')} + aria-label={removeLabel} + data-test-subj={`minus-${field.name}-${bucket.value}`} + style={{ + minHeight: 'auto', + minWidth: 'auto', + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + className={'euiButtonIcon--auto'} + /> +
+
+ )} +
+ + + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.scss similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_field_details.scss rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.scss diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx new file mode 100644 index 000000000000..63d5c7ace303 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx @@ -0,0 +1,312 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { act } from '@testing-library/react'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { DiscoverFieldDetails } from './discover_field_details'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; + +const mockGetHref = jest.fn(); +const mockGetTriggerCompatibleActions = jest.fn(); + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getUiActions: () => ({ + getTriggerCompatibleActions: mockGetTriggerCompatibleActions, + }), +})); + +const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() +); + +describe('discover sidebar field details', function () { + const defaultProps = { + columns: [], + details: { buckets: [], error: '', exists: 1, total: 1 }, + indexPattern, + onAddFilter: jest.fn(), + }; + + beforeEach(() => { + mockGetHref.mockReturnValue('/foo/bar'); + mockGetTriggerCompatibleActions.mockReturnValue([ + { + getHref: mockGetHref, + }, + ]); + }); + + function mountComponent(field: IndexPatternField, props?: Record) { + const compProps = { ...defaultProps, ...props, field }; + return mountWithIntl(); + } + + it('should render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const buckets = [1, 2, 3].map((n) => ({ + display: `display-${n}`, + value: `value-${n}`, + percent: 25, + count: 100, + })); + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, buckets }, + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + // Visualize link should not be rendered until async hook update + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + // Complete async hook + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe( + buckets.length + ); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should only render buckets if they exist', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + + await act(async () => { + await nextTick(); + comp.update(); + }); + + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should render a details error', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const errText = 'Some error'; + const comp = mountComponent(visualizableField, { + details: { ...defaultProps.details, error: errText }, + }); + expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualizeError').text()).toBe(errText); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should handle promise rejection from isFieldVisualizable', async function () { + mockGetTriggerCompatibleActions.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should handle promise rejection from getVisualizeHref', async function () { + mockGetHref.mockRejectedValue(new Error('Async error')); + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0); + }); + + it('should enable the visualize link for a number field', async function () { + const visualizableField = new IndexPatternField( + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'bytes' + ); + const comp = mountComponent(visualizableField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1); + expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1); + }); + + it('should disable the visualize link for an _id field', async function () { + expect.assertions(1); + const conflictField = new IndexPatternField( + { + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(conflictField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-_id').length).toBe(0); + }); + + it('should disable the visualize link for an unknown field', async function () { + const unknownField = new IndexPatternField( + { + name: 'test', + type: 'unknown', + esTypes: ['double'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + 'test' + ); + const comp = mountComponent(unknownField); + + await act(async () => { + await nextTick(); + comp.update(); + }); + expect(findTestSubject(comp, 'fieldVisualize-test').length).toBe(0); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx new file mode 100644 index 000000000000..906c173ed07d --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx @@ -0,0 +1,153 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { DiscoverFieldBucket } from './discover_field_bucket'; +import { getWarnings } from './lib/get_warnings'; +import { + triggerVisualizeActions, + isFieldVisualizable, + getVisualizeHref, +} from './lib/visualize_trigger_utils'; +import { Bucket, FieldDetails } from './types'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; +import './discover_field_details.scss'; + +interface DiscoverFieldDetailsProps { + columns: string[]; + details: FieldDetails; + field: IndexPatternField; + indexPattern: IndexPattern; + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} + +export function DiscoverFieldDetails({ + columns, + details, + field, + indexPattern, + onAddFilter, +}: DiscoverFieldDetailsProps) { + const warnings = getWarnings(field); + const [showVisualizeLink, setShowVisualizeLink] = useState(false); + const [visualizeLink, setVisualizeLink] = useState(''); + + useEffect(() => { + const checkIfVisualizable = async () => { + const visualizable = await isFieldVisualizable(field, indexPattern.id, columns).catch( + () => false + ); + + setShowVisualizeLink(visualizable); + if (visualizable) { + const href = await getVisualizeHref(field, indexPattern.id, columns).catch(() => ''); + setVisualizeLink(href || ''); + } + }; + checkIfVisualizable(); + }, [field, indexPattern.id, columns]); + + const handleVisualizeLinkClick = (event: React.MouseEvent) => { + // regular link click. let the uiActions code handle the navigation and show popup if needed + event.preventDefault(); + triggerVisualizeActions(field, indexPattern.id, columns); + }; + + return ( + <> +
+ {details.error && ( + + {details.error} + + )} + + {!details.error && details.buckets.length > 0 && ( +
+ {details.buckets.map((bucket: Bucket, idx: number) => ( + + ))} +
+ )} + + {showVisualizeLink && visualizeLink && ( +
+ + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + handleVisualizeLinkClick(e)} + href={visualizeLink} + size="s" + className="dscFieldDetails__visualizeBtn" + data-test-subj={`fieldVisualize-${field.name}`} + > + + + {warnings.length > 0 && ( + + )} +
+ )} +
+ {!details.error && ( + + + {!indexPattern.metaFields.includes(field.name) && !field.scripted ? ( + onAddFilter('_exists_', field.name, '+')}> + {' '} + {details.exists} + + ) : ( + {details.exists} + )}{' '} + / {details.total}{' '} + + + + )} + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx new file mode 100644 index 000000000000..f78505e11f1e --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx @@ -0,0 +1,160 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/helpers'; +import { DiscoverFieldSearch, Props } from './discover_field_search'; +import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; +import { ReactWrapper } from 'enzyme'; + +describe('DiscoverFieldSearch', () => { + const defaultProps = { + onChange: jest.fn(), + value: 'test', + types: ['any', 'string', '_source'], + }; + + function mountComponent(props?: Props) { + const compProps = props || defaultProps; + return mountWithIntl(); + } + + function findButtonGroup(component: ReactWrapper, id: string) { + return component.find(`[data-test-subj="${id}ButtonGroup"]`).first(); + } + + test('enter value', () => { + const component = mountComponent(); + const input = findTestSubject(component, 'fieldFilterSearchInput'); + input.simulate('change', { target: { value: 'new filter' } }); + expect(defaultProps.onChange).toBeCalledTimes(1); + }); + + test('change in active filters should change facet selection and call onChange', () => { + const onChange = jest.fn(); + const component = mountComponent({ ...defaultProps, ...{ onChange } }); + let btn = findTestSubject(component, 'toggleFieldFilterButton'); + expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy(); + btn.simulate('click'); + const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); + act(() => { + // @ts-ignore + (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); + }); + component.update(); + btn = findTestSubject(component, 'toggleFieldFilterButton'); + expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true); + expect(onChange).toBeCalledWith('aggregatable', true); + }); + + test('change in active filters should change filters count', () => { + const component = mountComponent(); + let btn = findTestSubject(component, 'toggleFieldFilterButton'); + btn.simulate('click'); + btn = findTestSubject(component, 'toggleFieldFilterButton'); + const badge = btn.find('.euiNotificationBadge'); + // no active filters + expect(badge.text()).toEqual('0'); + // change value of aggregatable select + const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); + act(() => { + // @ts-ignore + (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); + }); + component.update(); + expect(badge.text()).toEqual('1'); + // change value of searchable select + const searchableButtonGroup = findButtonGroup(component, 'searchable'); + act(() => { + // @ts-ignore + (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null); + }); + component.update(); + expect(badge.text()).toEqual('2'); + // change value of searchable select + act(() => { + // @ts-ignore + (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null); + }); + component.update(); + expect(badge.text()).toEqual('1'); + }); + + test('change in missing fields switch should not change filter count', () => { + const component = mountComponent(); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + btn.simulate('click'); + const badge = btn.find('.euiNotificationBadge'); + expect(badge.text()).toEqual('0'); + const missingSwitch = findTestSubject(component, 'missingSwitch'); + missingSwitch.simulate('change', { target: { value: false } }); + expect(badge.text()).toEqual('0'); + }); + + test('change in filters triggers onChange', () => { + const onChange = jest.fn(); + const component = mountComponent({ ...defaultProps, ...{ onChange } }); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + btn.simulate('click'); + const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable'); + const missingSwitch = findTestSubject(component, 'missingSwitch'); + act(() => { + // @ts-ignore + (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); + }); + missingSwitch.simulate('click'); + expect(onChange).toBeCalledTimes(2); + }); + + test('change in type filters triggers onChange with appropriate value', () => { + const onChange = jest.fn(); + const component = mountComponent({ ...defaultProps, ...{ onChange } }); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + btn.simulate('click'); + const typeSelector = findTestSubject(component, 'typeSelect'); + typeSelector.simulate('change', { target: { value: 'string' } }); + expect(onChange).toBeCalledWith('type', 'string'); + typeSelector.simulate('change', { target: { value: 'any' } }); + expect(onChange).toBeCalledWith('type', 'any'); + }); + + test('click on filter button should open and close popover', () => { + const component = mountComponent(); + const btn = findTestSubject(component, 'toggleFieldFilterButton'); + btn.simulate('click'); + let popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(true); + btn.simulate('click'); + popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(false); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx new file mode 100644 index 000000000000..4a1390cb1955 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx @@ -0,0 +1,313 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiFacetButton, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSelect, + EuiSwitch, + EuiSwitchEvent, + EuiForm, + EuiFormRow, + EuiButtonGroup, + EuiOutsideClickDetector, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +export interface State { + searchable: string; + aggregatable: string; + type: string; + missing: boolean; + [index: string]: string | boolean; +} + +export interface Props { + /** + * triggered on input of user into search field + */ + onChange: (field: string, value: string | boolean | undefined) => void; + + /** + * the input value of the user + */ + value?: string; + + /** + * types for the type filter + */ + types: string[]; +} + +/** + * Component is Discover's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function DiscoverFieldSearch({ onChange, value, types }: Props) { + const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search field names', + }); + const aggregatableLabel = i18n.translate('discover.fieldChooser.filter.aggregatableLabel', { + defaultMessage: 'Aggregatable', + }); + const searchableLabel = i18n.translate('discover.fieldChooser.filter.searchableLabel', { + defaultMessage: 'Searchable', + }); + const typeLabel = i18n.translate('discover.fieldChooser.filter.typeLabel', { + defaultMessage: 'Type', + }); + const typeOptions = types + ? types.map((type) => { + return { value: type, text: type }; + }) + : [{ value: 'any', text: 'any' }]; + + const [activeFiltersCount, setActiveFiltersCount] = useState(0); + const [isPopoverOpen, setPopoverOpen] = useState(false); + const [values, setValues] = useState({ + searchable: 'any', + aggregatable: 'any', + type: 'any', + missing: true, + }); + + if (typeof value !== 'string') { + // at initial rendering value is undefined (angular related), this catches the warning + // should be removed once all is react + return null; + } + + const filterBtnAriaLabel = isPopoverOpen + ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { + defaultMessage: 'Hide field filter settings', + }) + : i18n.translate('discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { + defaultMessage: 'Show field filter settings', + }); + + const handleFacetButtonClicked = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const applyFilterValue = (id: string, filterValue: string | boolean) => { + switch (filterValue) { + case 'any': + if (id !== 'type') { + onChange(id, undefined); + } else { + onChange(id, filterValue); + } + break; + case 'true': + onChange(id, true); + break; + case 'false': + onChange(id, false); + break; + default: + onChange(id, filterValue); + } + }; + + const isFilterActive = (name: string, filterValue: string | boolean) => { + return name !== 'missing' && filterValue !== 'any'; + }; + + const handleValueChange = (name: string, filterValue: string | boolean) => { + const previousValue = values[name]; + updateFilterCount(name, previousValue, filterValue); + const updatedValues = { ...values }; + updatedValues[name] = filterValue; + setValues(updatedValues); + applyFilterValue(name, filterValue); + }; + + const updateFilterCount = ( + name: string, + previousValue: string | boolean, + currentValue: string | boolean + ) => { + const previouslyFilterActive = isFilterActive(name, previousValue); + const filterActive = isFilterActive(name, currentValue); + const diff = Number(filterActive) - Number(previouslyFilterActive); + setActiveFiltersCount(activeFiltersCount + diff); + }; + + const handleMissingChange = (e: EuiSwitchEvent) => { + const missingValue = e.target.checked; + handleValueChange('missing', missingValue); + }; + + const buttonContent = ( + } + isSelected={activeFiltersCount > 0} + quantity={activeFiltersCount} + onClick={handleFacetButtonClicked} + > + + + ); + + const select = ( + id: string, + selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>, + selectValue: string + ) => { + return ( + ) => + handleValueChange(id, e.target.value) + } + aria-label={i18n.translate('discover.fieldChooser.filter.fieldSelectorLabel', { + defaultMessage: 'Selection of {id} filter options', + values: { id }, + })} + data-test-subj={`${id}Select`} + compressed + /> + ); + }; + + const toggleButtons = (id: string) => { + return [ + { + id: `${id}-any`, + label: 'any', + }, + { + id: `${id}-true`, + label: 'yes', + }, + { + id: `${id}-false`, + label: 'no', + }, + ]; + }; + + const buttonGroup = (id: string, legend: string) => { + return ( + handleValueChange(id, optionId.replace(`${id}-`, ''))} + buttonSize="compressed" + isFullWidth + data-test-subj={`${id}ButtonGroup`} + /> + ); + }; + + const selectionPanel = ( +
+ + + {buttonGroup('aggregatable', aggregatableLabel)} + + + {buttonGroup('searchable', searchableLabel)} + + + {select('type', typeOptions, values.type)} + + +
+ ); + + return ( + + + + onChange('name', event.currentTarget.value)} + placeholder={searchPlaceholder} + value={value} + /> + + +
+ {}} isDisabled={!isPopoverOpen}> + { + setPopoverOpen(false); + }} + button={buttonContent} + > + + {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', { + defaultMessage: 'Filter by type', + })} + + {selectionPanel} + + + + + +
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.test.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.test.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.tsx diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern_title.tsx similarity index 100% rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern_title.tsx diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss new file mode 100644 index 000000000000..9c80e0afa600 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss @@ -0,0 +1,99 @@ +.dscSidebar__container { + padding-left: 0 !important; + padding-right: 0 !important; + background-color: transparent; + border-right-color: transparent; + border-bottom-color: transparent; +} + +.dscIndexPattern__container { + display: flex; + align-items: center; + height: $euiSize * 3; + margin-top: -$euiSizeS; +} + +.dscIndexPattern__triggerButton { + @include euiTitle("xs"); + + line-height: $euiSizeXXL; +} + +.dscFieldList { + list-style: none; + margin-bottom: 0; +} + +.dscFieldListHeader { + padding: $euiSizeS $euiSizeS 0 $euiSizeS; + background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); +} + +.dscFieldList--popular { + background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); +} + +.dscFieldChooser { + padding-left: $euiSize; +} + +.dscFieldChooser__toggle { + color: $euiColorMediumShade; + margin-left: $euiSizeS !important; +} + +.dscSidebarItem { + &:hover, + &:focus-within, + &[class*="-isActive"] { + .dscSidebarItem__action { + opacity: 1; + } + } +} + +/** + * 1. Only visually hide the action, so that it's still accessible to screen readers. + * 2. When tabbed to, this element needs to be visible for keyboard accessibility. + */ +.dscSidebarItem__action { + opacity: 0; /* 1 */ + transition: none; + + &:focus { + opacity: 1; /* 2 */ + } + + font-size: $euiFontSizeXS; + padding: 2px 6px !important; + height: 22px !important; + min-width: auto !important; + + .euiButton__content { + padding: 0 4px; + } +} + +.dscFieldSearch { + padding: $euiSizeS; +} + +.dscFieldSearch__toggleButton { + width: calc(100% - #{$euiSizeS}); + color: $euiColorPrimary; + padding-left: $euiSizeXS; + margin-left: $euiSizeXS; +} + +.dscFieldSearch__filterWrapper { + flex-grow: 0; +} + +.dscFieldSearch__formWrapper { + padding: $euiSizeM; +} + +.dscFieldDetails { + color: $euiTextColor; + margin-bottom: $euiSizeS; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx new file mode 100644 index 000000000000..fa692ca22b5b --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { ReactWrapper } from 'enzyme'; +import { findTestSubject } from 'test_utils/helpers'; +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { SavedObject } from '../../../../../../core/types'; + +jest.mock('../../../opensearch_dashboards_services', () => ({ + getServices: () => ({ + history: () => ({ + location: { + search: '', + }, + }), + capabilities: { + visualize: { + show: true, + }, + discover: { + save: false, + }, + }, + uiSettings: { + get: (key: string) => { + if (key === 'fields:popularLimit') { + return 5; + } else if (key === 'shortDots:enable') { + return false; + } + }, + }, + }), +})); + +jest.mock('./lib/get_index_pattern_field_list', () => ({ + getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), +})); + +function getCompProps() { + const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + + // @ts-expect-error _.each() is passing additional args to flattenHit + const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array< + Record + >; + + const indexPatternList = [ + { id: '0', attributes: { title: 'b' } } as SavedObject, + { id: '1', attributes: { title: 'a' } } as SavedObject, + { id: '2', attributes: { title: 'c' } } as SavedObject, + ]; + + const fieldCounts: Record = {}; + + for (const hit of hits) { + for (const key of Object.keys(indexPattern.flattenHit(hit))) { + fieldCounts[key] = (fieldCounts[key] || 0) + 1; + } + } + return { + columns: ['extension'], + fieldCounts, + hits, + indexPatternList, + onAddFilter: jest.fn(), + onAddField: jest.fn(), + onRemoveField: jest.fn(), + selectedIndexPattern: indexPattern, + setIndexPattern: jest.fn(), + state: {}, + }; +} + +describe('discover sidebar', function () { + let props: DiscoverSidebarProps; + let comp: ReactWrapper; + + beforeAll(() => { + props = getCompProps(); + comp = mountWithIntl(); + }); + + it('should have Selected Fields and Available Fields with Popular Fields sections', function () { + const popular = findTestSubject(comp, 'fieldList-popular'); + const selected = findTestSubject(comp, 'fieldList-selected'); + const unpopular = findTestSubject(comp, 'fieldList-unpopular'); + expect(popular.children().length).toBe(1); + expect(unpopular.children().length).toBe(7); + expect(selected.children().length).toBe(1); + }); + it('should allow selecting fields', function () { + findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); + }); + it('should allow deselecting fields', function () { + findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + expect(props.onRemoveField).toHaveBeenCalledWith('extension'); + }); + it('should allow adding filters', function () { + findTestSubject(comp, 'field-extension-showDetails').simulate('click'); + findTestSubject(comp, 'plus-extension-gif').simulate('click'); + expect(props.onAddFilter).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx new file mode 100644 index 000000000000..865aff590286 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx @@ -0,0 +1,326 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './discover_sidebar.scss'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { sortBy } from 'lodash'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { DiscoverField } from './discover_field'; +import { DiscoverIndexPattern } from './discover_index_pattern'; +import { DiscoverFieldSearch } from './discover_field_search'; +import { IndexPatternAttributes } from '../../../../../data/common'; +import { SavedObject } from '../../../../../../core/types'; +import { FIELDS_LIMIT_SETTING } from '../../../../common'; +import { groupFields } from './lib/group_fields'; +import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; +import { getDetails } from './lib/get_details'; +import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; +import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; +import { getServices } from '../../../opensearch_dashboards_services'; + +export interface DiscoverSidebarProps { + /** + * the selected columns displayed in the doc table in discover + */ + columns: string[]; + /** + * a statistics of the distribution of fields in the given hits + */ + fieldCounts: Record; + /** + * hits fetched from OpenSearch, displayed in the doc table + */ + hits: Array>; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * Callback function when selecting a field + */ + onAddField: (fieldName: string) => void; + /** + * Callback function when adding a filter from sidebar + */ + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + /** + * Callback function when removing a field + * @param fieldName + */ + onRemoveField: (fieldName: string) => void; + /** + * Currently selected index pattern + */ + selectedIndexPattern?: IndexPattern; + /** + * Callback function to select another index pattern + */ + setIndexPattern: (id: string) => void; +} + +export function DiscoverSidebar({ + columns, + fieldCounts, + hits, + indexPatternList, + onAddField, + onAddFilter, + onRemoveField, + selectedIndexPattern, + setIndexPattern, +}: DiscoverSidebarProps) { + const [showFields, setShowFields] = useState(false); + const [fields, setFields] = useState(null); + const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); + const services = useMemo(() => getServices(), []); + + useEffect(() => { + const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); + setFields(newFields); + }, [selectedIndexPattern, fieldCounts, hits, services]); + + const onChangeFieldSearch = useCallback( + (field: string, value: string | boolean | undefined) => { + const newState = setFieldFilterProp(fieldFilterState, field, value); + setFieldFilterState(newState); + }, + [fieldFilterState] + ); + + const getDetailsByField = useCallback( + (ipField: IndexPatternField) => getDetails(ipField, hits, selectedIndexPattern), + [hits, selectedIndexPattern] + ); + + const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); + const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + + const { + selected: selectedFields, + popular: popularFields, + unpopular: unpopularFields, + } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [ + fields, + columns, + popularLimit, + fieldCounts, + fieldFilterState, + ]); + + const fieldTypes = useMemo(() => { + const result = ['any']; + if (Array.isArray(fields)) { + for (const field of fields) { + if (result.indexOf(field.type) === -1) { + result.push(field.type); + } + } + } + return result; + }, [fields]); + + if (!selectedIndexPattern || !fields) { + return null; + } + + return ( + +
+ o.attributes.title)} + /> +
+
+ + +
+
+ {fields.length > 0 && ( + <> + +

+ +

+
+ +
    + {selectedFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
+
+ +

+ +

+
+
+ setShowFields(!showFields)} + aria-label={ + showFields + ? i18n.translate( + 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', + { + defaultMessage: 'Hide fields', + } + ) + : i18n.translate( + 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', + { + defaultMessage: 'Show fields', + } + ) + } + /> +
+
+ + )} + {popularFields.length > 0 && ( +
+ + + +
    + {popularFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
+
+ )} + +
    + {unpopularFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
+
+
+
+ ); +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/index.ts b/src/plugins/discover_legacy/public/application/components/sidebar/index.ts new file mode 100644 index 000000000000..2799d47da83f --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/index.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DiscoverSidebar } from './discover_sidebar'; diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts new file mode 100644 index 000000000000..d580f7ae228a --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts @@ -0,0 +1,268 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +// @ts-ignore +import realHits from 'fixtures/real_hits.js'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { coreMock } from '../../../../../../../core/public/mocks'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; +import { + groupValues, + getFieldValues, + getFieldValueCounts, + FieldValueCountsParams, +} from './field_calculator'; +import { Bucket } from '../types'; + +let indexPattern: IndexPattern; + +describe('field_calculator', function () { + beforeEach(function () { + indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() + ); + }); + + describe('groupValues', function () { + let groups: Record; + let grouped: boolean; + let values: any[]; + beforeEach(function () { + values = [ + ['foo', 'bar'], + 'foo', + 'foo', + undefined, + ['foo', 'bar'], + 'bar', + 'baz', + null, + null, + null, + 'foo', + undefined, + ]; + groups = groupValues(values, grouped); + }); + + it('should return an object values', function () { + expect(groups).toBeInstanceOf(Object); + }); + + it('should throw an error if any value is a plain object', function () { + expect(function () { + groupValues([{}, true, false], grouped); + }).toThrowError(); + }); + + it('should handle values with dots in them', function () { + values = ['0', '0.........', '0.......,.....']; + groups = groupValues(values, grouped); + expect(groups[values[0]].count).toBe(1); + expect(groups[values[1]].count).toBe(1); + expect(groups[values[2]].count).toBe(1); + }); + + it('should have a key for value in the array when not grouping array terms', function () { + expect(_.keys(groups).length).toBe(3); + expect(groups.foo).toBeInstanceOf(Object); + expect(groups.bar).toBeInstanceOf(Object); + expect(groups.baz).toBeInstanceOf(Object); + }); + + it('should count array terms independently', function () { + expect(groups['foo,bar']).toBeUndefined(); + expect(groups.foo.count).toBe(5); + expect(groups.bar.count).toBe(3); + expect(groups.baz.count).toBe(1); + }); + + describe('grouped array terms', function () { + beforeEach(function () { + grouped = true; + groups = groupValues(values, grouped); + }); + + it('should group array terms when grouped is true', function () { + expect(_.keys(groups).length).toBe(4); + expect(groups['foo,bar']).toBeInstanceOf(Object); + }); + + it('should contain the original array as the value', function () { + expect(groups['foo,bar'].value).toEqual(['foo', 'bar']); + }); + + it('should count the pairs separately from the values they contain', function () { + expect(groups['foo,bar'].count).toBe(2); + expect(groups.foo.count).toBe(3); + expect(groups.bar.count).toBe(1); + }); + }); + }); + + describe('getFieldValues', function () { + let hits: any; + + beforeEach(function () { + hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); + }); + + it('should return an array of values for _source fields', function () { + const extensions = getFieldValues({ + hits, + field: indexPattern.fields.getByName('extension') as IndexPatternField, + indexPattern, + }); + expect(extensions).toBeInstanceOf(Array); + expect( + _.filter(extensions, function (v) { + return v === 'html'; + }).length + ).toBe(8); + expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']); + }); + + it('should return an array of values for core meta fields', function () { + const types = getFieldValues({ + hits, + field: indexPattern.fields.getByName('_type') as IndexPatternField, + indexPattern, + }); + expect(types).toBeInstanceOf(Array); + expect( + _.filter(types, function (v) { + return v === 'apache'; + }).length + ).toBe(18); + expect(_.uniq(_.clone(types)).sort()).toEqual(['apache', 'nginx']); + }); + }); + + describe('getFieldValueCounts', function () { + let params: FieldValueCountsParams; + beforeEach(function () { + params = { + hits: _.cloneDeep(realHits), + field: indexPattern.fields.getByName('extension') as IndexPatternField, + count: 3, + indexPattern, + }; + }); + + it('counts the top 5 values by default', function () { + params.hits = params.hits.map((hit: Record, i) => ({ + ...hit, + _source: { + extension: `${hit._source.extension}-${i}`, + }, + })); + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(5); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than default', function () { + params.count = undefined; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + + it('counts only distinct values if less than specified count', function () { + params.count = 10; + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(4); + expect(extensions.error).toBeUndefined(); + }); + + it('counts the top 3 values', function () { + const extensions = getFieldValueCounts(params); + expect(extensions).toBeInstanceOf(Object); + expect(extensions.buckets).toBeInstanceOf(Array); + const buckets = extensions.buckets as Bucket[]; + expect(buckets.length).toBe(3); + expect(_.map(buckets, 'value')).toEqual(['html', 'gif', 'php']); + expect(extensions.error).toBeUndefined(); + }); + + it('fails to analyze geo and attachment types', function () { + params.field = indexPattern.fields.getByName('point') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + + params.field = indexPattern.fields.getByName('area') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + + params.field = indexPattern.fields.getByName('request_body') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + }); + + it('fails to analyze fields that are in the mapping, but not the hits', function () { + params.field = indexPattern.fields.getByName('ip') as IndexPatternField; + expect(getFieldValueCounts(params).error).not.toBeUndefined(); + }); + + it('counts the total hits', function () { + expect(getFieldValueCounts(params).total).toBe(params.hits.length); + }); + + it('counts the hits the field exists in', function () { + params.field = indexPattern.fields.getByName('phpmemory') as IndexPatternField; + expect(getFieldValueCounts(params).exists).toBe(5); + }); + + it('catches and returns errors', function () { + params.hits = params.hits.map((hit: Record) => ({ + ...hit, + _source: { + extension: { foo: hit._source.extension }, + }, + })); + params.grouped = true; + expect(typeof getFieldValueCounts(params).error).toBe('string'); + }); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts new file mode 100644 index 000000000000..54f8832fa1fc --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; +import { FieldValueCounts } from '../types'; + +const NO_ANALYSIS_TYPES = ['geo_point', 'geo_shape', 'attachment']; + +interface FieldValuesParams { + hits: Array>; + field: IndexPatternField; + indexPattern: IndexPattern; +} + +interface FieldValueCountsParams extends FieldValuesParams { + count?: number; + grouped?: boolean; +} + +const getFieldValues = ({ hits, field, indexPattern }: FieldValuesParams) => { + const name = field.name; + const flattenHit = indexPattern.flattenHit; + return hits.map((hit) => flattenHit(hit)[name]); +}; + +const getFieldValueCounts = (params: FieldValueCountsParams): FieldValueCounts => { + const { hits, field, indexPattern, count = 5, grouped = false } = params; + const { type: fieldType } = field; + + if (NO_ANALYSIS_TYPES.includes(fieldType)) { + return { + error: i18n.translate( + 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for {fieldType} fields.', + values: { + fieldType, + }, + } + ), + }; + } + + const allValues = getFieldValues({ hits, field, indexPattern }); + const missing = allValues.filter((v) => v === undefined || v === null).length; + + try { + const groups = groupValues(allValues, grouped); + const counts = Object.keys(groups) + .sort((a, b) => groups[b].count - groups[a].count) + .slice(0, count) + .map((key) => ({ + value: groups[key].value, + count: groups[key].count, + percent: (groups[key].count / (hits.length - missing)) * 100, + display: indexPattern.getFormatterForField(field).convert(groups[key].value), + })); + + if (hits.length === missing) { + return { + error: i18n.translate( + 'discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage', + { + defaultMessage: + 'This field is present in your OpenSearch mapping but not in the {hitsLength} documents shown in the doc table. You may still be able to visualize or search on it.', + values: { + hitsLength: hits.length, + }, + } + ), + }; + } + + return { + total: hits.length, + exists: hits.length - missing, + missing, + buckets: counts, + }; + } catch (e) { + return { + error: e instanceof Error ? e.message : String(e), + }; + } +}; + +const groupValues = ( + allValues: any[], + grouped?: boolean +): Record => { + const values = grouped ? allValues : allValues.flat(); + + return values + .filter((v) => { + if (v instanceof Object && !Array.isArray(v)) { + throw new Error( + i18n.translate( + 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage', + { + defaultMessage: 'Analysis is not available for object fields.', + } + ) + ); + } + return v !== undefined && v !== null; + }) + .reduce((groups, value) => { + if (groups.hasOwnProperty(value)) { + groups[value].count++; + } else { + groups[value] = { + value, + count: 1, + }; + } + return groups; + }, {}); +}; + +export { FieldValueCountsParams, groupValues, getFieldValues, getFieldValueCounts }; diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts new file mode 100644 index 000000000000..a21d93cb5bc4 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter'; +import { IndexPatternField } from '../../../../../../data/public'; + +describe('field_filter', function () { + it('getDefaultFieldFilter should return default filter state', function () { + expect(getDefaultFieldFilter()).toMatchInlineSnapshot(` + Object { + "aggregatable": null, + "missing": true, + "name": "", + "searchable": null, + "type": "any", + } + `); + }); + it('setFieldFilterProp should return allow filter changes', function () { + const state = getDefaultFieldFilter(); + const targetState = { + aggregatable: true, + missing: true, + name: 'test', + searchable: true, + type: 'string', + }; + const actualState = Object.entries(targetState).reduce((acc, kv) => { + return setFieldFilterProp(acc, kv[0], kv[1]); + }, state); + expect(actualState).toMatchInlineSnapshot(` + Object { + "aggregatable": true, + "missing": true, + "name": "test", + "searchable": true, + "type": "string", + } + `); + }); + it('filters a given list', () => { + const defaultState = getDefaultFieldFilter(); + const fieldList = [ + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: false, + aggregatable: false, + }, + { + name: 'extension', + type: 'string', + esTypes: ['text'], + count: 10, + scripted: true, + searchable: true, + aggregatable: true, + }, + ] as IndexPatternField[]; + + [ + { filter: {}, result: ['bytes', 'extension'] }, + { filter: { name: 'by' }, result: ['bytes'] }, + { filter: { aggregatable: true }, result: ['extension'] }, + { filter: { aggregatable: true, searchable: false }, result: [] }, + { filter: { type: 'string' }, result: ['extension'] }, + ].forEach((test) => { + const filtered = fieldList + .filter((field) => + isFieldFiltered(field, { ...defaultState, ...test.filter }, { bytes: 1, extension: 1 }) + ) + .map((field) => field.name); + + expect(filtered).toEqual(test.result); + }); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts new file mode 100644 index 000000000000..d72af29b43e0 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPatternField } from '../../../../../../data/public'; + +export interface FieldFilterState { + missing: boolean; + type: string; + name: string; + aggregatable: null | boolean; + searchable: null | boolean; +} + +export function getDefaultFieldFilter(): FieldFilterState { + return { + missing: true, + type: 'any', + name: '', + aggregatable: null, + searchable: null, + }; +} + +export function setFieldFilterProp( + state: FieldFilterState, + name: string, + value: string | boolean | null | undefined +): FieldFilterState { + const newState = { ...state }; + if (name === 'missing') { + newState.missing = Boolean(value); + } else if (name === 'aggregatable') { + newState.aggregatable = typeof value !== 'boolean' ? null : value; + } else if (name === 'searchable') { + newState.searchable = typeof value !== 'boolean' ? null : value; + } else if (name === 'name') { + newState.name = String(value); + } else if (name === 'type') { + newState.type = String(value); + } + return newState; +} + +export function isFieldFiltered( + field: IndexPatternField, + filterState: FieldFilterState, + fieldCounts: Record +): boolean { + const matchFilter = filterState.type === 'any' || field.type === filterState.type; + const isAggregatable = + filterState.aggregatable === null || field.aggregatable === filterState.aggregatable; + const isSearchable = + filterState.searchable === null || field.searchable === filterState.searchable; + const scriptedOrMissing = + !filterState.missing || + field.type === '_source' || + field.scripted || + fieldCounts[field.name] > 0; + const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1; + + return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts new file mode 100644 index 000000000000..823cbde9ba72 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { i18n } from '@osd/i18n'; +import { getFieldValueCounts } from './field_calculator'; +import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; + +export function getDetails( + field: IndexPatternField, + hits: Array>, + indexPattern?: IndexPattern +) { + const defaultDetails = { + error: '', + exists: 0, + total: 0, + buckets: [], + }; + if (!indexPattern) { + return { + ...defaultDetails, + error: i18n.translate('discover.fieldChooser.noIndexPatternSelectedErrorMessage', { + defaultMessage: 'Index pattern not specified.', + }), + }; + } + const details = { + ...defaultDetails, + ...getFieldValueCounts({ + hits, + field, + indexPattern, + count: 5, + grouped: false, + }), + }; + if (details.buckets) { + for (const bucket of details.buckets) { + bucket.display = indexPattern.getFormatterForField(field).convert(bucket.value); + } + } + return details; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts new file mode 100644 index 000000000000..38b18792d3e4 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; + +export function getFieldTypeName(type: string) { + switch (type) { + case 'boolean': + return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { + defaultMessage: 'Boolean field', + }); + case 'conflict': + return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', { + defaultMessage: 'Conflicting field', + }); + case 'date': + return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { + defaultMessage: 'Date field', + }); + case 'geo_point': + return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { + defaultMessage: 'Geo point field', + }); + case 'geo_shape': + return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { + defaultMessage: 'Geo shape field', + }); + case 'ip': + return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { + defaultMessage: 'IP address field', + }); + case 'murmur3': + return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { + defaultMessage: 'Murmur3 field', + }); + case 'number': + return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { + defaultMessage: 'Number field', + }); + case 'source': + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + case 'string': + return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { + defaultMessage: 'String field', + }); + case 'nested': + return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', { + defaultMessage: 'Nested field', + }); + default: + return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', { + defaultMessage: 'Unknown field', + }); + } +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts new file mode 100644 index 000000000000..b3a8ff5cd8d9 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { difference } from 'lodash'; +import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; + +export function getIndexPatternFieldList( + indexPattern?: IndexPattern, + fieldCounts?: Record +) { + if (!indexPattern || !fieldCounts) return []; + + const fieldNamesInDocs = Object.keys(fieldCounts); + const fieldNamesInIndexPattern = indexPattern.fields.getAll().map((fld) => fld.name); + const unknownTypes: IndexPatternField[] = []; + + difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach((unknownFieldName) => { + unknownTypes.push({ + displayName: String(unknownFieldName), + name: String(unknownFieldName), + type: 'unknown', + } as IndexPatternField); + }); + + return [...indexPattern.fields.getAll(), ...unknownTypes]; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts new file mode 100644 index 000000000000..770a0ce664e4 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { IndexPatternField } from '../../../../../../data/public'; + +export function getWarnings(field: IndexPatternField) { + let warnings = []; + + if (field.scripted) { + warnings.push( + i18n.translate( + 'discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription', + { + defaultMessage: 'Scripted fields can take a long time to execute.', + } + ) + ); + } + + if (warnings.length > 1) { + warnings = warnings.map(function (warning, i) { + return (i > 0 ? '\n' : '') + (i + 1) + ' - ' + warning; + }); + } + + return warnings; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts new file mode 100644 index 000000000000..7301ce3a4c96 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { groupFields } from './group_fields'; +import { getDefaultFieldFilter } from './field_filter'; + +describe('group_fields', function () { + it('should group fields in selected, popular, unpopular group', function () { + const fields = [ + { + name: 'category', + type: 'string', + esTypes: ['text'], + count: 1, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'currency', + type: 'string', + esTypes: ['keyword'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + { + name: 'customer_birth_date', + type: 'date', + esTypes: ['date'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ]; + + const fieldCounts = { + category: 1, + currency: 1, + customer_birth_date: 1, + }; + + const fieldFilterState = getDefaultFieldFilter(); + + const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState); + expect(actual).toMatchInlineSnapshot(` + Object { + "popular": Array [ + Object { + "aggregatable": true, + "count": 1, + "esTypes": Array [ + "text", + ], + "name": "category", + "readFromDocValues": true, + "scripted": false, + "searchable": true, + "type": "string", + }, + ], + "selected": Array [ + Object { + "aggregatable": true, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "name": "currency", + "readFromDocValues": true, + "scripted": false, + "searchable": true, + "type": "string", + }, + ], + "unpopular": Array [ + Object { + "aggregatable": true, + "count": 0, + "esTypes": Array [ + "date", + ], + "name": "customer_birth_date", + "readFromDocValues": true, + "scripted": false, + "searchable": true, + "type": "date", + }, + ], + } + `); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx new file mode 100644 index 000000000000..fad1db402467 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPatternField } from 'src/plugins/data/public'; +import { FieldFilterState, isFieldFiltered } from './field_filter'; + +interface GroupedFields { + selected: IndexPatternField[]; + popular: IndexPatternField[]; + unpopular: IndexPatternField[]; +} + +/** + * group the fields into selected, popular and unpopular, filter by fieldFilterState + */ +export function groupFields( + fields: IndexPatternField[] | null, + columns: string[], + popularLimit: number, + fieldCounts: Record, + fieldFilterState: FieldFilterState +): GroupedFields { + const result: GroupedFields = { + selected: [], + popular: [], + unpopular: [], + }; + if (!Array.isArray(fields) || !Array.isArray(columns) || typeof fieldCounts !== 'object') { + return result; + } + + const popular = fields + .filter((field) => !columns.includes(field.name) && field.count) + .sort((a: IndexPatternField, b: IndexPatternField) => (b.count || 0) - (a.count || 0)) + .map((field) => field.name) + .slice(0, popularLimit); + + const compareFn = (a: IndexPatternField, b: IndexPatternField) => { + if (!a.displayName) { + return 0; + } + return a.displayName.localeCompare(b.displayName || ''); + }; + const fieldsSorted = fields.sort(compareFn); + + for (const field of fieldsSorted) { + if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) { + continue; + } + if (columns.includes(field.name)) { + result.selected.push(field); + } else if (popular.includes(field.name) && field.type !== '_source') { + result.popular.push(field); + } else if (field.type !== '_source') { + result.unpopular.push(field); + } + } + + return result; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts new file mode 100644 index 000000000000..36a6bcf2e329 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + VISUALIZE_FIELD_TRIGGER, + VISUALIZE_GEO_FIELD_TRIGGER, + visualizeFieldTrigger, + visualizeGeoFieldTrigger, +} from '../../../../../../ui_actions/public'; +import { getUiActions } from '../../../../opensearch_dashboards_services'; +import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../../data/public'; + +function getTriggerConstant(type: string) { + return type === OSD_FIELD_TYPES.GEO_POINT || type === OSD_FIELD_TYPES.GEO_SHAPE + ? VISUALIZE_GEO_FIELD_TRIGGER + : VISUALIZE_FIELD_TRIGGER; +} + +function getTrigger(type: string) { + return type === OSD_FIELD_TYPES.GEO_POINT || type === OSD_FIELD_TYPES.GEO_SHAPE + ? visualizeGeoFieldTrigger + : visualizeFieldTrigger; +} + +async function getCompatibleActions( + fieldName: string, + indexPatternId: string, + contextualFields: string[], + trigger: typeof VISUALIZE_FIELD_TRIGGER | typeof VISUALIZE_GEO_FIELD_TRIGGER +) { + const compatibleActions = await getUiActions().getTriggerCompatibleActions(trigger, { + indexPatternId, + fieldName, + contextualFields, + }); + return compatibleActions; +} + +export async function getVisualizeHref( + field: IndexPatternField, + indexPatternId: string | undefined, + contextualFields: string[] +) { + if (!indexPatternId) return undefined; + const triggerOptions = { + indexPatternId, + fieldName: field.name, + contextualFields, + trigger: getTrigger(field.type), + }; + const compatibleActions = await getCompatibleActions( + field.name, + indexPatternId, + contextualFields, + getTriggerConstant(field.type) + ); + // enable the link only if only one action is registered + return compatibleActions.length === 1 + ? compatibleActions[0].getHref?.(triggerOptions) + : undefined; +} + +export function triggerVisualizeActions( + field: IndexPatternField, + indexPatternId: string | undefined, + contextualFields: string[] +) { + if (!indexPatternId) return; + const trigger = getTriggerConstant(field.type); + const triggerOptions = { + indexPatternId, + fieldName: field.name, + contextualFields, + }; + getUiActions().getTrigger(trigger).exec(triggerOptions); +} + +export async function isFieldVisualizable( + field: IndexPatternField, + indexPatternId: string | undefined, + contextualFields: string[] +) { + if (field.name === '_id' || !indexPatternId) { + // for first condition you'd get a 'Fielddata access on the _id field is disallowed' error on OpenSearch side. + return false; + } + const trigger = getTriggerConstant(field.type); + const compatibleActions = await getCompatibleActions( + field.name, + indexPatternId, + contextualFields, + trigger + ); + return compatibleActions.length > 0 && field.visualizable; +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx new file mode 100644 index 000000000000..dba087d0f9ed --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiProgress } from '@elastic/eui'; + +interface Props { + percent: number; + count: number; + value: string; +} + +export function StringFieldProgressBar({ value, percent, count }: Props) { + const ariaLabel = `${value}: ${count} (${percent}%)`; + + return ( + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/types.ts b/src/plugins/discover_legacy/public/application/components/sidebar/types.ts new file mode 100644 index 000000000000..a43120b28e96 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/sidebar/types.ts @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface IndexPatternRef { + id: string; + title: string; +} + +export interface FieldDetails { + error: string; + exists: number; + total: number; + buckets: Bucket[]; +} + +export interface FieldValueCounts extends Partial { + missing?: number; +} + +export interface Bucket { + display: string; + value: string; + percent: number; + count: number; +} diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts new file mode 100644 index 000000000000..094d8e286875 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { SkipBottomButton } from './skip_bottom_button'; diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx new file mode 100644 index 000000000000..28ffef9dae86 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { SkipBottomButton, SkipBottomButtonProps } from './skip_bottom_button'; + +describe('Skip to Bottom Button', function () { + let props: SkipBottomButtonProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + onClick: jest.fn(), + }; + }); + + it('should be clickable', function () { + component = mountWithIntl(); + component.simulate('click'); + expect(props.onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx new file mode 100644 index 000000000000..a1e5754cb312 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiSkipLink } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; + +export interface SkipBottomButtonProps { + /** + * Action to perform on click + */ + onClick: () => void; +} + +export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { + return ( + + { + // prevent the anchor to reload the page on click + event.preventDefault(); + // The destinationId prop cannot be leveraged here as the table needs + // to be updated first (angular logic) + onClick(); + }} + className="dscSkipButton" + destinationId="" + data-test-subj="discoverSkipTableButton" + > + + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table.test.tsx b/src/plugins/discover_legacy/public/application/components/table/table.test.tsx new file mode 100644 index 000000000000..220ac57feae2 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table.test.tsx @@ -0,0 +1,279 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { findTestSubject } from 'test_utils/helpers'; +import { DocViewTable } from './table'; +import { indexPatterns, IndexPattern } from '../../../../../data/public'; + +const indexPattern = ({ + fields: { + getAll: () => [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + { + name: 'extension', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'bytes', + type: 'number', + scripted: false, + filterable: true, + }, + { + name: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, + ], + }, + metaFields: ['_index', '_score'], + flattenHit: undefined, + formatHit: jest.fn((hit) => hit._source), +} as unknown) as IndexPattern; + +indexPattern.fields.getByName = (name: string) => { + return indexPattern.fields.getAll().find((field) => field.name === name); +}; + +indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); + +describe('DocViewTable at Discover', () => { + // At Discover's main view, all buttons are rendered + // check for existence of action buttons and warnings + + const hit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, + _source: { + message: + 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \ + et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \ + ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \ + Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \ + rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \ + Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \ + Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', + extension: 'html', + not_mapped: 'yes', + bytes: 100, + objectArray: [{ foo: true }], + relatedContent: { + test: 1, + }, + scripted: 123, + _underscore: 123, + }, + }; + + const props = { + hit, + columns: ['extension'], + indexPattern, + filter: jest.fn(), + onAddColumn: jest.fn(), + onRemoveColumn: jest.fn(), + }; + const component = mount(); + [ + { + _property: '_index', + addInclusiveFilterButton: true, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underscoreWarning: false, + }, + { + _property: 'message', + addInclusiveFilterButton: false, + collapseBtn: true, + noMappingWarning: false, + toggleColumnButton: true, + underscoreWarning: false, + }, + { + _property: '_underscore', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underScoreWarning: true, + }, + { + _property: 'scripted', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: false, + toggleColumnButton: true, + underScoreWarning: false, + }, + { + _property: 'not_mapped', + addInclusiveFilterButton: false, + collapseBtn: false, + noMappingWarning: true, + toggleColumnButton: true, + underScoreWarning: false, + }, + ].forEach((check) => { + const rowComponent = findTestSubject(component, `tableDocViewRow-${check._property}`); + + it(`renders row for ${check._property}`, () => { + expect(rowComponent.length).toBe(1); + }); + + ([ + 'addInclusiveFilterButton', + 'collapseBtn', + 'toggleColumnButton', + 'underscoreWarning', + ] as const).forEach((element) => { + const elementExist = check[element]; + + if (typeof elementExist === 'boolean') { + const btn = findTestSubject(rowComponent, element); + + it(`renders ${element} for '${check._property}' correctly`, () => { + const disabled = btn.length ? btn.props().disabled : true; + const clickAble = btn.length && !disabled ? true : false; + expect(clickAble).toBe(elementExist); + }); + } + }); + + (['noMappingWarning'] as const).forEach((element) => { + const elementExist = check[element]; + + if (typeof elementExist === 'boolean') { + const el = findTestSubject(rowComponent, element); + + it(`renders ${element} for '${check._property}' correctly`, () => { + expect(el.length).toBe(elementExist ? 1 : 0); + }); + } + }); + }); +}); + +describe('DocViewTable at Discover Doc', () => { + const hit = { + _index: 'logstash-2014.09.09', + _score: 1, + _type: 'doc', + _id: 'id123', + _source: { + extension: 'html', + not_mapped: 'yes', + }, + }; + // here no action buttons are rendered + const props = { + hit, + indexPattern, + }; + const component = mount(); + const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length; + + it(`renders no action buttons`, () => { + expect(foundLength).toBe(0); + }); +}); + +describe('DocViewTable at Discover Context', () => { + // here no toggleColumnButtons are rendered + const hit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, + _source: { + message: + 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ + Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \ + et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \ + ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \ + Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \ + rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \ + Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \ + Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', + }, + }; + const props = { + hit, + columns: ['extension'], + indexPattern, + filter: jest.fn(), + }; + + const component = mount(); + + it(`renders no toggleColumnButton`, () => { + const foundLength = findTestSubject(component, 'toggleColumnButtons').length; + expect(foundLength).toBe(0); + }); + + it(`renders addInclusiveFilterButton`, () => { + const row = findTestSubject(component, `tableDocViewRow-_index`); + const btn = findTestSubject(row, 'addInclusiveFilterButton'); + expect(btn.length).toBe(1); + btn.simulate('click'); + expect(props.filter).toBeCalled(); + }); + + it(`renders functional collapse button`, () => { + const btn = findTestSubject(component, `collapseBtn`); + const html = component.html(); + + expect(component.html()).toContain('truncate-by-height'); + + expect(btn.length).toBe(1); + btn.simulate('click'); + expect(component.html() !== html).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/table/table.tsx b/src/plugins/discover_legacy/public/application/components/table/table.tsx new file mode 100644 index 000000000000..90167a515985 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table.tsx @@ -0,0 +1,149 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { escapeRegExp } from 'lodash'; +import { DocViewTableRow } from './table_row'; +import { arrayContainsObjects, trimAngularSpan } from './table_helper'; +import { DocViewRenderProps } from '../../doc_views/doc_views_types'; + +const COLLAPSE_LINE_LENGTH = 350; + +export function DocViewTable({ + hit, + indexPattern, + filter, + columns, + onAddColumn, + onRemoveColumn, +}: DocViewRenderProps) { + const mapping = indexPattern.fields.getByName; + const flattened = indexPattern.flattenHit(hit); + const formatted = indexPattern.formatHit(hit, 'html'); + const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); + + function toggleValueCollapse(field: string) { + fieldRowOpen[field] = fieldRowOpen[field] !== true; + setFieldRowOpen({ ...fieldRowOpen }); + } + + return ( + + + {Object.keys(flattened) + .sort() + .map((field) => { + const valueRaw = flattened[field]; + const value = trimAngularSpan(String(formatted[field])); + + const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; + const isCollapsed = isCollapsible && !fieldRowOpen[field]; + const toggleColumn = + onRemoveColumn && onAddColumn && Array.isArray(columns) + ? () => { + if (columns.includes(field)) { + onRemoveColumn(field); + } else { + onAddColumn(field); + } + } + : undefined; + const isArrayOfObjects = + Array.isArray(flattened[field]) && arrayContainsObjects(flattened[field]); + const displayUnderscoreWarning = !mapping(field) && field.indexOf('_') === 0; + const displayNoMappingWarning = + !mapping(field) && !displayUnderscoreWarning && !isArrayOfObjects; + + // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that + // contains an array, Discover will only detect the top level root field. We want to detect when those + // root fields are `nested` so that we can display the proper icon and label. However, those root + // `nested` fields are not a part of the index pattern. Their children are though, and contain nested path + // info. So to detect nested fields we look through the index pattern for nested children + // whose path begins with the current field. There are edge cases where + // this could incorrectly identify a plain `object` field as `nested`. Say we had the following document + // where `foo` is a plain object field and `bar` is a nested field. + // { + // "foo": [ + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // }, + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // } + // ] + // } + // + // The following code will search for `foo`, find it at the beginning of the path to the nested child field + // `foo.bar.baz` and incorrectly mark `foo` as nested. Any time we're searching for the name of a plain object + // field that happens to match a segment of a nested path, we'll get a false positive. + // We're aware of this issue and we'll have to live with + // it in the short term. The long term fix will be to add info about the `nested` and `object` root fields + // to the index pattern, but that has its own complications which you can read more about in the following + // issue: https://github.com/elastic/kibana/issues/54957 + const isNestedField = + !indexPattern.fields.getByName(field) && + !!indexPattern.fields.getAll().find((patternField) => { + // We only want to match a full path segment + const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); + return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); + }); + const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type; + + return ( + toggleValueCollapse(field)} + onToggleColumn={toggleColumn} + value={value} + valueRaw={valueRaw} + /> + ); + })} + +
+ ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts b/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts new file mode 100644 index 000000000000..20c1092ef86d --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { arrayContainsObjects } from './table_helper'; + +describe('arrayContainsObjects', () => { + it(`returns false for an array of primitives`, () => { + const actual = arrayContainsObjects(['test', 'test']); + expect(actual).toBeFalsy(); + }); + + it(`returns true for an array of objects`, () => { + const actual = arrayContainsObjects([{}, {}]); + expect(actual).toBeTruthy(); + }); + + it(`returns true for an array of objects and primitves`, () => { + const actual = arrayContainsObjects([{}, 'sdf']); + expect(actual).toBeTruthy(); + }); + + it(`returns false for an array of null values`, () => { + const actual = arrayContainsObjects([null, null]); + expect(actual).toBeFalsy(); + }); + + it(`returns false if no array is given`, () => { + const actual = arrayContainsObjects([null, null]); + expect(actual).toBeFalsy(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx b/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx new file mode 100644 index 000000000000..2e63b43b8310 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Returns true if the given array contains at least 1 object + */ +export function arrayContainsObjects(value: unknown[]): boolean { + return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null); +} + +/** + * Removes markup added by OpenSearch Dashboards fields html formatter + */ +export function trimAngularSpan(text: string): string { + return text.replace(/^/, '').replace(/<\/span>$/, ''); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row.tsx new file mode 100644 index 000000000000..95ba38106e3e --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row.tsx @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import classNames from 'classnames'; +import React, { ReactNode } from 'react'; +import { FieldMapping, DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; +import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; +import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; +import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse'; +import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; +import { DocViewTableRowIconNoMapping } from './table_row_icon_no_mapping'; +import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore'; +import { FieldName } from '../field_name/field_name'; + +export interface Props { + field: string; + fieldMapping?: FieldMapping; + fieldType: string; + displayNoMappingWarning: boolean; + displayUnderscoreWarning: boolean; + isCollapsible: boolean; + isColumnActive: boolean; + isCollapsed: boolean; + onToggleCollapse: () => void; + onFilter?: DocViewFilterFn; + onToggleColumn?: () => void; + value: string | ReactNode; + valueRaw: unknown; +} + +export function DocViewTableRow({ + field, + fieldMapping, + fieldType, + displayNoMappingWarning, + displayUnderscoreWarning, + isCollapsible, + isCollapsed, + isColumnActive, + onFilter, + onToggleCollapse, + onToggleColumn, + value, + valueRaw, +}: Props) { + const valueClassName = classNames({ + // eslint-disable-next-line @typescript-eslint/naming-convention + osdDocViewer__value: true, + 'truncate-by-height': isCollapsible && isCollapsed, + }); + + return ( + + {typeof onFilter === 'function' && ( + + onFilter(fieldMapping, valueRaw, '+')} + /> + onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + + )} + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + + )} + + + + + {isCollapsible && ( + + )} + {displayUnderscoreWarning && } + {displayNoMappingWarning && } +
+ + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx new file mode 100644 index 000000000000..de25c73e9c95 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; + +export interface Props { + onClick: () => void; + isCollapsed: boolean; +} + +export function DocViewTableRowBtnCollapse({ onClick, isCollapsed }: Props) { + const label = i18n.translate('discover.docViews.table.toggleFieldDetails', { + defaultMessage: 'Toggle field details', + }); + return ( + + onClick()} + iconType={isCollapsed ? 'arrowRight' : 'arrowDown'} + iconSize={'s'} + /> + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx new file mode 100644 index 000000000000..1707861faf28 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled: boolean; +} + +export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx new file mode 100644 index 000000000000..d4f401282e14 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx @@ -0,0 +1,81 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; + scripted?: boolean; +} + +export function DocViewTableRowBtnFilterExists({ + onClick, + disabled = false, + scripted = false, +}: Props) { + const tooltipContent = disabled ? ( + scripted ? ( + + ) : ( + + ) + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx new file mode 100644 index 000000000000..3b58fbfdc282 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; +} + +export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx new file mode 100644 index 000000000000..74f0972fa0ee --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + active: boolean; + disabled?: boolean; + onClick: () => void; +} + +export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = false }: Props) { + if (disabled) { + return ( + + ); + } + return ( + + } + > + + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx new file mode 100644 index 000000000000..edc4bea91bd8 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export function DocViewTableRowIconNoMapping() { + const ariaLabel = i18n.translate('discover.docViews.table.noCachedMappingForThisFieldAriaLabel', { + defaultMessage: 'Warning', + }); + const tooltipContent = i18n.translate( + 'discover.docViews.table.noCachedMappingForThisFieldTooltip', + { + defaultMessage: + 'No cached mapping for this field. Refresh field list from the Management > Index Patterns page', + } + ); + return ( + + ); +} diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx new file mode 100644 index 000000000000..f1d09e2c8d44 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export function DocViewTableRowIconUnderscore() { + const ariaLabel = i18n.translate( + 'discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel', + { + defaultMessage: 'Warning', + } + ); + const tooltipContent = i18n.translate( + 'discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip', + { + defaultMessage: 'Field names beginning with {underscoreSign} are not supported', + values: { underscoreSign: '_' }, + } + ); + + return ( + + ); +} diff --git a/src/plugins/discover/public/application/components/timechart_header/index.ts b/src/plugins/discover_legacy/public/application/components/timechart_header/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/timechart_header/index.ts rename to src/plugins/discover_legacy/public/application/components/timechart_header/index.ts diff --git a/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx new file mode 100644 index 000000000000..9011c38a6acb --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; +import { EuiIconTip } from '@elastic/eui'; +import { findTestSubject } from 'test_utils/helpers'; + +describe('timechart header', function () { + let props: TimechartHeaderProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + timeRange: { + from: 'May 14, 2020 @ 11:05:13.590', + to: 'May 14, 2020 @ 11:20:13.590', + }, + stateInterval: 's', + options: [ + { + display: 'Auto', + val: 'auto', + }, + { + display: 'Millisecond', + val: 'ms', + }, + { + display: 'Second', + val: 's', + }, + ], + onChangeInterval: jest.fn(), + bucketInterval: { + scaled: undefined, + description: 'second', + scale: undefined, + }, + }; + }); + + it('TimechartHeader not renders an info text when the showScaledInfo property is not provided', () => { + component = mountWithIntl(); + expect(component.find(EuiIconTip).length).toBe(0); + }); + + it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => { + props.bucketInterval!.scaled = true; + component = mountWithIntl(); + expect(component.find(EuiIconTip).length).toBe(1); + }); + + it('expect to render the date range', function () { + component = mountWithIntl(); + const datetimeRangeText = findTestSubject(component, 'discoverIntervalDateRange'); + expect(datetimeRangeText.text()).toBe( + 'May 14, 2020 @ 11:05:13.590 - May 14, 2020 @ 11:20:13.590 per' + ); + }); + + it('expects to render a dropdown with the interval options', () => { + component = mountWithIntl(); + const dropdown = findTestSubject(component, 'discoverIntervalSelect'); + expect(dropdown.length).toBe(1); + // @ts-ignore + const values = dropdown.find('option').map((option) => option.prop('value')); + expect(values).toEqual(['auto', 'ms', 's']); + // @ts-ignore + const labels = dropdown.find('option').map((option) => option.text()); + expect(labels).toEqual(['Auto', 'Millisecond', 'Second']); + }); + + it('should change the interval', function () { + component = mountWithIntl(); + findTestSubject(component, 'discoverIntervalSelect').simulate('change', { + target: { value: 'ms' }, + }); + expect(props.onChangeInterval).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.tsx similarity index 100% rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx rename to src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.tsx diff --git a/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap new file mode 100644 index 000000000000..342dea206c30 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render 1`] = ` + + + +

+ +

+
+
+ + + } + onChoose={[Function]} + savedObjectMetaData={ + Array [ + Object { + "getIconForSavedObject": [Function], + "name": "Saved search", + "type": "search", + }, + ] + } + savedObjects={Object {}} + uiSettings={Object {}} + /> + + + + + + + + + + +
+`; diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.js b/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.js rename to src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js b/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.test.js similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js rename to src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.test.js diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js b/src/plugins/discover_legacy/public/application/components/top_nav/show_open_search_panel.js similarity index 100% rename from src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js rename to src/plugins/discover_legacy/public/application/components/top_nav/show_open_search_panel.js diff --git a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx b/src/plugins/discover_legacy/public/application/doc_views/doc_views_helpers.tsx similarity index 100% rename from src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx rename to src/plugins/discover_legacy/public/application/doc_views/doc_views_helpers.tsx diff --git a/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts new file mode 100644 index 000000000000..56f167b5f2cc --- /dev/null +++ b/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { auto } from 'angular'; +import { convertDirectiveToRenderFn } from './doc_views_helpers'; +import { DocView, DocViewInput, OpenSearchSearchHit, DocViewInputFn } from './doc_views_types'; + +export class DocViewsRegistry { + private docViews: DocView[] = []; + private angularInjectorGetter: (() => Promise) | null = null; + + setAngularInjectorGetter = (injectorGetter: () => Promise) => { + this.angularInjectorGetter = injectorGetter; + }; + + /** + * Extends and adds the given doc view to the registry array + */ + addDocView(docViewRaw: DocViewInput | DocViewInputFn) { + const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; + if (docView.directive) { + // convert angular directive to render function for backwards compatibility + docView.render = convertDirectiveToRenderFn(docView.directive, () => { + if (!this.angularInjectorGetter) { + throw new Error('Angular was not initialized'); + } + return this.angularInjectorGetter(); + }); + } + if (typeof docView.shouldShow !== 'function') { + docView.shouldShow = () => true; + } + this.docViews.push(docView as DocView); + } + /** + * Returns a sorted array of doc_views for rendering tabs + */ + getDocViewsSorted(hit: OpenSearchSearchHit) { + return this.docViews + .filter((docView) => docView.shouldShow(hit)) + .sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1)); + } +} diff --git a/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts b/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts new file mode 100644 index 000000000000..961fc98516f6 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ComponentType } from 'react'; +import { IScope } from 'angular'; +import { SearchResponse } from 'elasticsearch'; +import { IndexPattern } from '../../../../data/public'; + +export interface AngularDirective { + controller: (...injectedServices: any[]) => void; + template: string; +} + +export type AngularScope = IScope; + +export type OpenSearchSearchHit = SearchResponse['hits']['hits'][number]; + +export interface FieldMapping { + filterable?: boolean; + scripted?: boolean; + rowCount?: number; + type: string; + name: string; +} + +export type DocViewFilterFn = ( + mapping: FieldMapping | string | undefined, + value: unknown, + mode: '+' | '-' +) => void; + +export interface DocViewRenderProps { + columns?: string[]; + filter?: DocViewFilterFn; + hit: OpenSearchSearchHit; + indexPattern: IndexPattern; + onAddColumn?: (columnName: string) => void; + onRemoveColumn?: (columnName: string) => void; +} +export type DocViewerComponent = ComponentType; +export type DocViewRenderFn = ( + domeNode: HTMLDivElement, + renderProps: DocViewRenderProps +) => () => void; + +export interface DocViewInput { + component?: DocViewerComponent; + directive?: AngularDirective; + order: number; + render?: DocViewRenderFn; + shouldShow?: (hit: OpenSearchSearchHit) => boolean; + title: string; +} + +export interface DocView extends DocViewInput { + shouldShow: (hit: OpenSearchSearchHit) => boolean; +} + +export type DocViewInputFn = () => DocViewInput; diff --git a/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts new file mode 100644 index 000000000000..16653f5d5377 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DocViewLink } from './doc_views_links_types'; + +export class DocViewsLinksRegistry { + private docViewsLinks: DocViewLink[] = []; + + addDocViewLink(docViewLink: DocViewLink) { + this.docViewsLinks.push(docViewLink); + } + + getDocViewsLinksSorted() { + return this.docViewsLinks.sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1)); + } +} diff --git a/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts new file mode 100644 index 000000000000..bbc5caadafcd --- /dev/null +++ b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiListGroupItemProps } from '@elastic/eui'; +import { OpenSearchSearchHit } from '../doc_views/doc_views_types'; +import { IndexPattern } from '../../../../data/public'; + +export interface DocViewLink extends EuiListGroupItemProps { + href?: string; + order: number; + generateCb?( + renderProps: any + ): { + url: string; + hide?: boolean; + }; +} + +export interface DocViewLinkRenderProps { + columns?: string[]; + hit: OpenSearchSearchHit; + indexPattern: IndexPattern; +} diff --git a/src/plugins/discover/public/application/embeddable/constants.ts b/src/plugins/discover_legacy/public/application/embeddable/constants.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/constants.ts rename to src/plugins/discover_legacy/public/application/embeddable/constants.ts diff --git a/src/plugins/discover/public/application/embeddable/index.ts b/src/plugins/discover_legacy/public/application/embeddable/index.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/index.ts rename to src/plugins/discover_legacy/public/application/embeddable/index.ts diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.scss b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable.scss similarity index 100% rename from src/plugins/discover/public/application/embeddable/search_embeddable.scss rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable.scss diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/search_embeddable.ts rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable.ts diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable_factory.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable_factory.ts diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover_legacy/public/application/embeddable/search_template.html similarity index 100% rename from src/plugins/discover/public/application/embeddable/search_template.html rename to src/plugins/discover_legacy/public/application/embeddable/search_template.html diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover_legacy/public/application/embeddable/types.ts similarity index 100% rename from src/plugins/discover/public/application/embeddable/types.ts rename to src/plugins/discover_legacy/public/application/embeddable/types.ts diff --git a/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts b/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts new file mode 100644 index 000000000000..e30f50206aef --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; + +export function getRootBreadcrumbs() { + return [ + { + text: i18n.translate('discover.rootBreadcrumb', { + defaultMessage: 'Discover', + }), + href: '#/', + }, + ]; +} + +export function getSavedSearchBreadcrumbs($route: any) { + return [ + ...getRootBreadcrumbs(), + { + text: $route.current.locals.savedObjects.savedSearch.id, + }, + ]; +} diff --git a/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts b/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts new file mode 100644 index 000000000000..b1b3c96e0958 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const COMMA_SEPARATOR_RE = /(\d)(?=(\d{3})+(?!\d))/g; + +/** + * Converts a number to a string and adds commas + * as thousands separators + */ +export const formatNumWithCommas = (input: number) => + String(input).replace(COMMA_SEPARATOR_RE, '$1,'); diff --git a/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts b/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts new file mode 100644 index 000000000000..dfb02c0b0740 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IIndexPattern } from '../../../../data/common/index_patterns'; + +export function findIndexPatternById( + indexPatterns: IIndexPattern[], + id: string +): IIndexPattern | undefined { + if (!Array.isArray(indexPatterns) || !id) { + return; + } + return indexPatterns.find((o) => o.id === id); +} + +/** + * Checks if the given defaultIndex exists and returns + * the first available index pattern id if not + */ +export function getFallbackIndexPatternId( + indexPatterns: IIndexPattern[], + defaultIndex: string = '' +): string { + if (defaultIndex && findIndexPatternById(indexPatterns, defaultIndex)) { + return defaultIndex; + } + return !indexPatterns || !indexPatterns.length || !indexPatterns[0].id ? '' : indexPatterns[0].id; +} + +/** + * A given index pattern id is checked for existence and a fallback is provided if it doesn't exist + * The provided defaultIndex is usually configured in Advanced Settings, if it's also invalid + * the first entry of the given list of Indexpatterns is used + */ +export function getIndexPatternId( + id: string = '', + indexPatterns: IIndexPattern[], + defaultIndex: string = '' +): string { + if (!id || !findIndexPatternById(indexPatterns, id)) { + return getFallbackIndexPatternId(indexPatterns, defaultIndex); + } + return id; +} diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts b/src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.test.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts rename to src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.test.ts diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts b/src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.ts similarity index 100% rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts rename to src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.ts diff --git a/src/plugins/discover_legacy/public/application/helpers/index.ts b/src/plugins/discover_legacy/public/application/helpers/index.ts new file mode 100644 index 000000000000..d765fdf60cee --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/index.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { shortenDottedString } from './shorten_dotted_string'; +export { formatNumWithCommas } from './format_number_with_commas'; diff --git a/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts new file mode 100644 index 000000000000..90458c135b98 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { has } from 'lodash'; +import { Query } from 'src/plugins/data/public'; + +/** + * Creates a standardized query object from old queries that were either strings or pure OpenSearch query DSL + * + * @param query - a legacy query, what used to be stored in SearchSource's query property + * @return Object + */ + +export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { + // Lucene was the only option before, so language-less queries are all lucene + if (!has(query, 'language')) { + return { query, language: 'lucene' }; + } + + return query as Query; +} diff --git a/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts b/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts new file mode 100644 index 000000000000..cdd49c0f77f1 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern, IndexPatternsService } from '../../../../data/public'; +import { popularizeField } from './popularize_field'; + +describe('Popularize field', () => { + test('returns undefined if index pattern lacks id', async () => { + const indexPattern = ({} as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({} as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + expect(result).toBeUndefined(); + }); + + test('returns undefined if field not found', async () => { + const indexPattern = ({ + fields: { + getByName: () => {}, + }, + } as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({} as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + expect(result).toBeUndefined(); + }); + + test('returns undefined if successful', async () => { + const field = { + count: 0, + }; + const indexPattern = ({ + id: 'id', + fields: { + getByName: () => field, + }, + } as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({ + updateSavedObject: async () => {}, + } as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + expect(result).toBeUndefined(); + expect(field.count).toEqual(1); + }); + + test('hides errors', async () => { + const field = { + count: 0, + }; + const indexPattern = ({ + id: 'id', + fields: { + getByName: () => field, + }, + } as unknown) as IndexPattern; + const fieldName = '@timestamp'; + const indexPatternsService = ({ + updateSavedObject: async () => { + throw new Error('unknown error'); + }, + } as unknown) as IndexPatternsService; + const result = await popularizeField(indexPattern, fieldName, indexPatternsService); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts b/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts new file mode 100644 index 000000000000..e7c4b900fa19 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern, IndexPatternsService } from '../../../../data/public'; + +async function popularizeField( + indexPattern: IndexPattern, + fieldName: string, + indexPatternsService: IndexPatternsService +) { + if (!indexPattern.id) return; + const field = indexPattern.fields.getByName(fieldName); + if (!field) { + return; + } + + field.count++; + // Catch 409 errors caused by user adding columns in a higher frequency that the changes can be persisted to OpenSearch + try { + await indexPatternsService.updateSavedObject(indexPattern, 0, true); + // eslint-disable-next-line no-empty + } catch {} +} + +export { popularizeField }; diff --git a/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts b/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts new file mode 100644 index 000000000000..39450f8c82c0 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const DOT_PREFIX_RE = /(.).+?\./g; + +/** + * Convert a dot.notated.string into a short + * version (d.n.string) + */ +export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); diff --git a/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts new file mode 100644 index 000000000000..902f3d8a4b62 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { validateTimeRange } from './validate_time_range'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; + +describe('Discover validateTimeRange', () => { + test('validates given time ranges correctly', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + [ + { from: '', to: '', result: false }, + { from: 'now', to: 'now+1h', result: true }, + { from: 'now', to: 'lala+1h', result: false }, + { from: '', to: 'now', result: false }, + { from: 'now', to: '', result: false }, + { from: ' 2020-06-02T13:36:13.689Z', to: 'now', result: true }, + { from: ' 2020-06-02T13:36:13.689Z', to: '2020-06-02T13:36:13.690Z', result: true }, + ].map((test) => { + expect(validateTimeRange({ from: test.from, to: test.to }, toasts)).toEqual(test.result); + }); + }); + + test('displays a toast when invalid data is entered', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + expect(validateTimeRange({ from: 'now', to: 'null' }, toasts)).toEqual(false); + expect(toasts.addDanger).toHaveBeenCalledWith({ + title: 'Invalid time range', + text: "The provided time range is invalid. (from: 'now', to: 'null')", + }); + }); +}); diff --git a/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts new file mode 100644 index 000000000000..d23a84aabb14 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import dateMath from '@elastic/datemath'; +import { i18n } from '@osd/i18n'; +import { ToastsStart } from 'opensearch-dashboards/public'; + +/** + * Validates a given time filter range, provided by URL or UI + * Unless valid, it returns false and displays a notification + */ +export function validateTimeRange( + { from, to }: { from: string; to: string }, + toastNotifications: ToastsStart +): boolean { + const fromMoment = dateMath.parse(from); + const toMoment = dateMath.parse(to); + if (!fromMoment || !toMoment || !fromMoment.isValid() || !toMoment.isValid()) { + toastNotifications.addDanger({ + title: i18n.translate('discover.notifications.invalidTimeRangeTitle', { + defaultMessage: `Invalid time range`, + }), + text: i18n.translate('discover.notifications.invalidTimeRangeText', { + defaultMessage: `The provided time range is invalid. (from: '{from}', to: '{to}')`, + values: { + from, + to, + }, + }), + }); + return false; + } + return true; +} diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover_legacy/public/application/index.scss similarity index 100% rename from src/plugins/discover/public/application/index.scss rename to src/plugins/discover_legacy/public/application/index.scss diff --git a/src/plugins/discover_legacy/public/build_services.ts b/src/plugins/discover_legacy/public/build_services.ts new file mode 100644 index 000000000000..3fdafcff0c40 --- /dev/null +++ b/src/plugins/discover_legacy/public/build_services.ts @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { History } from 'history'; + +import { + Capabilities, + ChromeStart, + CoreStart, + DocLinksStart, + ToastsStart, + IUiSettingsClient, + PluginInitializerContext, +} from 'opensearch-dashboards/public'; +import { + FilterManager, + TimefilterContract, + IndexPatternsContract, + DataPublicPluginStart, +} from 'src/plugins/data/public'; +import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; +import { SharePluginStart } from 'src/plugins/share/public'; +import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { VisualizationsStart } from 'src/plugins/visualizations/public'; +import { SavedObjectOpenSearchDashboardsServices } from 'src/plugins/saved_objects/public'; + +import { DiscoverStartPlugins } from './plugin'; +import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; +import { getHistory } from './opensearch_dashboards_services'; +import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public'; +import { UrlForwardingStart } from '../../url_forwarding/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; + +export interface DiscoverServices { + addBasePath: (path: string) => string; + capabilities: Capabilities; + chrome: ChromeStart; + core: CoreStart; + data: DataPublicPluginStart; + docLinks: DocLinksStart; + history: () => History; + theme: ChartsPluginStart['theme']; + filterManager: FilterManager; + indexPatterns: IndexPatternsContract; + inspector: InspectorPublicPluginStart; + metadata: { branch: string }; + navigation: NavigationPublicPluginStart; + share?: SharePluginStart; + opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; + urlForwarding: UrlForwardingStart; + timefilter: TimefilterContract; + toastNotifications: ToastsStart; + getSavedSearchById: (id: string) => Promise; + getSavedSearchUrlById: (id: string) => Promise; + getEmbeddableInjector: any; + uiSettings: IUiSettingsClient; + visualizations: VisualizationsStart; +} + +export async function buildServices( + core: CoreStart, + plugins: DiscoverStartPlugins, + context: PluginInitializerContext, + getEmbeddableInjector: any +): Promise { + const services: SavedObjectOpenSearchDashboardsServices = { + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + search: plugins.data.search, + chrome: core.chrome, + overlays: core.overlays, + }; + const savedObjectService = createSavedSearchesLoader(services); + + return { + addBasePath: core.http.basePath.prepend, + capabilities: core.application.capabilities, + chrome: core.chrome, + core, + data: plugins.data, + docLinks: core.docLinks, + theme: plugins.charts.theme, + filterManager: plugins.data.query.filterManager, + getEmbeddableInjector, + getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), + history: getHistory, + indexPatterns: plugins.data.indexPatterns, + inspector: plugins.inspector, + metadata: { + branch: context.env.packageInfo.branch, + }, + navigation: plugins.navigation, + share: plugins.share, + opensearchDashboardsLegacy: plugins.opensearchDashboardsLegacy, + urlForwarding: plugins.urlForwarding, + timefilter: plugins.data.query.timefilter.timefilter, + toastNotifications: core.notifications.toasts, + uiSettings: core.uiSettings, + visualizations: plugins.visualizations, + }; +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover_legacy/public/get_inner_angular.ts similarity index 100% rename from src/plugins/discover/public/get_inner_angular.ts rename to src/plugins/discover_legacy/public/get_inner_angular.ts diff --git a/src/plugins/discover_legacy/public/index.ts b/src/plugins/discover_legacy/public/index.ts new file mode 100644 index 000000000000..6c9ab46b656e --- /dev/null +++ b/src/plugins/discover_legacy/public/index.ts @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'opensearch-dashboards/public'; +import { DiscoverPlugin } from './plugin'; + +export { DiscoverSetup, DiscoverStart } from './plugin'; +export function plugin(initializerContext: PluginInitializerContext) { + return new DiscoverPlugin(initializerContext); +} + +export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; +export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; +export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; diff --git a/src/plugins/discover_legacy/public/mocks.ts b/src/plugins/discover_legacy/public/mocks.ts new file mode 100644 index 000000000000..4724ced290ff --- /dev/null +++ b/src/plugins/discover_legacy/public/mocks.ts @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DiscoverSetup, DiscoverStart } from '.'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = { + docViews: { + addDocView: jest.fn(), + }, + docViewsLinks: { + addDocViewLink: jest.fn(), + }, + }; + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = { + savedSearchLoader: {} as any, + urlGenerator: { + createUrl: jest.fn(), + } as any, + }; + return startContract; +}; + +export const discoverPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts b/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts new file mode 100644 index 000000000000..8531564e0cc7 --- /dev/null +++ b/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import { createHashHistory } from 'history'; +import { ScopedHistory, AppMountParameters } from 'opensearch-dashboards/public'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { DiscoverServices } from './build_services'; +import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; +import { search } from '../../data/public'; +import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; + +let angularModule: any = null; +let services: DiscoverServices | null = null; +let uiActions: UiActionsStart; + +/** + * set bootstrapped inner angular module + */ +export function setAngularModule(module: any) { + angularModule = module; +} + +/** + * get boostrapped inner angular module + */ +export function getAngularModule() { + return angularModule; +} + +export function getServices(): DiscoverServices { + if (!services) { + throw new Error('Discover services are not yet available'); + } + return services; +} + +export function setServices(newServices: any) { + services = newServices; +} + +export const setUiActions = (pluginUiActions: UiActionsStart) => (uiActions = pluginUiActions); +export const getUiActions = () => uiActions; + +export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter< + AppMountParameters['setHeaderActionMenu'] +>('headerActionMenuMounter'); + +export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ + setTrackedUrl: (url: string) => void; + restorePreviousUrl: () => void; +}>('urlTracker'); + +export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( + 'DocViewsRegistry' +); + +export const [getDocViewsLinksRegistry, setDocViewsLinksRegistry] = createGetterSetter< + DocViewsLinksRegistry +>('DocViewsLinksRegistry'); +/** + * Makes sure discover and context are using one instance of history. + */ +export const getHistory = _.once(() => createHashHistory()); + +/** + * Discover currently uses two `history` instances: one from OpenSearch Dashboards Platform and + * another from `history` package. Below function is used every time Discover + * app is loaded to synchronize both instances. + * + * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved. + */ +export const syncHistoryLocations = () => { + const h = getHistory(); + Object.assign(h.location, createHashHistory().location); + return h; +}; + +export const [getScopedHistory, setScopedHistory] = createGetterSetter( + 'scopedHistory' +); + +export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; +export { unhashUrl, redirectWhenMissing } from '../../opensearch_dashboards_utils/public'; +export { + formatMsg, + formatStack, + subscribeWithScope, +} from '../../opensearch_dashboards_legacy/public'; + +// EXPORT types +export { + IndexPatternsContract, + IIndexPattern, + IndexPattern, + indexPatterns, + IFieldType, + ISearchSource, + OpenSearchQuerySortValue, + SortDirection, +} from '../../data/public'; diff --git a/src/plugins/discover_legacy/public/plugin.ts b/src/plugins/discover_legacy/public/plugin.ts new file mode 100644 index 000000000000..7e855b707891 --- /dev/null +++ b/src/plugins/discover_legacy/public/plugin.ts @@ -0,0 +1,487 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import angular, { auto } from 'angular'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +import { + AppMountParameters, + AppUpdater, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'opensearch-dashboards/public'; +import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; +import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; +import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { + OpenSearchDashboardsLegacySetup, + OpenSearchDashboardsLegacyStart, +} from 'src/plugins/opensearch_dashboards_legacy/public'; +import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; +import { stringify } from 'query-string'; +import rison from 'rison-node'; +import { NEW_DISCOVER_APP } from '../../discover/public'; +import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; +import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; +import { UrlGeneratorState } from '../../share/public'; +import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; +import { DocViewLink } from './application/doc_views_links/doc_views_links_types'; +import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; +import { DocViewTable } from './application/components/table/table'; +import { JsonCodeBlock } from './application/components/json_code_block/json_code_block'; +import { + setDocViewsRegistry, + setDocViewsLinksRegistry, + setUrlTracker, + setAngularModule, + setServices, + setHeaderActionMenuMounter, + setUiActions, + setScopedHistory, + getScopedHistory, + syncHistoryLocations, + getServices, +} from './opensearch_dashboards_services'; +import { createSavedSearchesLoader } from './saved_searches'; +import { buildServices } from './build_services'; +import { + DiscoverUrlGeneratorState, + DISCOVER_APP_URL_GENERATOR, + DiscoverUrlGenerator, +} from './url_generator'; +import { SearchEmbeddableFactory } from './application/embeddable'; +import { AppNavLinkStatus } from '../../../core/public'; +import { ViewRedirectParams } from '../../data_explorer/public'; + +declare module '../../share/public' { + export interface UrlGeneratorStateMapping { + [DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState; + } +} + +/** + * @public + */ +export interface DiscoverSetup { + docViews: { + /** + * Add new doc view shown along with table view and json view in the details of each document in Discover. + * Both react and angular doc views are supported. + * @param docViewRaw + */ + addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; + }; + + docViewsLinks: { + addDocViewLink(docViewLinkRaw: DocViewLink): void; + }; +} + +export interface DiscoverStart { + savedSearchLoader: SavedObjectLoader; + + /** + * `share` plugin URL generator for Discover app. Use it to generate links into + * Discover application, example: + * + * ```ts + * const url = await plugins.discover.urlGenerator.createUrl({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; +} + +/** + * @internal + */ +export interface DiscoverSetupPlugins { + share?: SharePluginSetup; + uiActions: UiActionsSetup; + embeddable: EmbeddableSetup; + opensearchDashboardsLegacy: OpenSearchDashboardsLegacySetup; + urlForwarding: UrlForwardingSetup; + home?: HomePublicPluginSetup; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** + * @internal + */ +export interface DiscoverStartPlugins { + uiActions: UiActionsStart; + embeddable: EmbeddableStart; + navigation: NavigationStart; + charts: ChartsPluginStart; + data: DataPublicPluginStart; + share?: SharePluginStart; + opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; + urlForwarding: UrlForwardingStart; + inspector: InspectorPublicPluginStart; + visualizations: VisualizationsStart; +} + +const innerAngularName = 'app/discover'; +const embeddableAngularName = 'app/discoverEmbeddable'; + +/** + * Contains Discover, one of the oldest parts of OpenSearch Dashboards + * There are 2 kinds of Angular bootstrapped for rendering, additionally to the main Angular + * Discover provides embeddables, those contain a slimmer Angular + */ +export class DiscoverPlugin + implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + private appStateUpdater = new BehaviorSubject(() => ({})); + private docViewsRegistry: DocViewsRegistry | null = null; + private docViewsLinksRegistry: DocViewsLinksRegistry | null = null; + private embeddableInjector: auto.IInjectorService | null = null; + private stopUrlTracking: (() => void) | undefined = undefined; + private servicesInitialized: boolean = false; + private innerAngularInitialized: boolean = false; + private urlGenerator?: DiscoverStart['urlGenerator']; + + /** + * why are those functions public? they are needed for some mocha tests + * can be removed once all is Jest + */ + public initializeInnerAngular?: () => void; + public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; + + setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + const baseUrl = core.http.basePath.prepend('/app/discover'); + + if (plugins.share) { + this.urlGenerator = plugins.share.urlGenerators.registerUrlGenerator( + new DiscoverUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + + this.docViewsRegistry = new DocViewsRegistry(); + setDocViewsRegistry(this.docViewsRegistry); + this.docViewsRegistry.addDocView({ + title: i18n.translate('discover.docViews.table.tableTitle', { + defaultMessage: 'Table', + }), + order: 10, + component: DocViewTable, + }); + + this.docViewsRegistry.addDocView({ + title: i18n.translate('discover.docViews.json.jsonTitle', { + defaultMessage: 'JSON', + }), + order: 20, + component: JsonCodeBlock, + }); + + this.docViewsLinksRegistry = new DocViewsLinksRegistry(); + setDocViewsLinksRegistry(this.docViewsLinksRegistry); + + this.docViewsLinksRegistry.addDocViewLink({ + label: i18n.translate('discover.docTable.tableRow.viewSurroundingDocumentsLinkText', { + defaultMessage: 'View surrounding documents', + }), + generateCb: (renderProps: any) => { + const globalFilters: any = getServices().filterManager.getGlobalFilters(); + const appFilters: any = getServices().filterManager.getAppFilters(); + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ + filters: globalFilters || [], + }), + _a: rison.encode({ + columns: renderProps.columns, + filters: (appFilters || []).map(opensearchFilters.disableFilter), + }), + }), + { encode: false, sort: false } + ); + + return { + url: `#/context/${encodeURIComponent(renderProps.indexPattern.id)}/${encodeURIComponent( + renderProps.hit._id + )}?${hash}`, + hide: !renderProps.indexPattern.isTimeBased(), + }; + }, + order: 1, + }); + + this.docViewsLinksRegistry.addDocViewLink({ + label: i18n.translate('discover.docTable.tableRow.viewSingleDocumentLinkText', { + defaultMessage: 'View single document', + }), + generateCb: (renderProps) => ({ + url: `#/doc/${renderProps.indexPattern.id}/${ + renderProps.hit._index + }?id=${encodeURIComponent(renderProps.hit._id)}`, + }), + order: 2, + }); + + const { + appMounted, + appUnMounted, + stop: stopUrlTracker, + setActiveUrl: setTrackedUrl, + restorePreviousUrl, + } = createOsdUrlTracker({ + // we pass getter here instead of plain `history`, + // so history is lazily created (when app is mounted) + // this prevents redundant `#` when not in discover app + getHistory: getScopedHistory, + baseUrl, + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:discover_legacy`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: plugins.data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + }); + setUrlTracker({ setTrackedUrl, restorePreviousUrl }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + + this.docViewsRegistry.setAngularInjectorGetter(this.getEmbeddableInjector); + core.application.register({ + id: 'discoverLegacy', + title: 'Discover Legacy', + defaultPath: '#/', + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + if (!this.initializeServices) { + throw Error('Discover plugin method initializeServices is undefined'); + } + if (!this.initializeInnerAngular) { + throw Error('Discover plugin method initializeInnerAngular is undefined'); + } + + // If a user explicitly tries to access the legacy app URL + const { + core: { + application: { navigateToApp }, + }, + } = await this.initializeServices(); + const path = window.location.hash; + + const v2Enabled = core.uiSettings.get(NEW_DISCOVER_APP); + if (v2Enabled) { + navigateToApp('discover', { + replace: true, + path, + }); + } + setScopedHistory(params.history); + setHeaderActionMenuMounter(params.setHeaderActionMenu); + syncHistoryLocations(); + appMounted(); + const { + plugins: { data: dataStart }, + } = await this.initializeServices(); + await this.initializeInnerAngular(); + + // make sure the index pattern list is up to date + await dataStart.indexPatterns.clearCache(); + const { renderApp } = await import('./application/application'); + params.element.classList.add('dscAppWrapper'); + const unmount = await renderApp(innerAngularName, params.element); + return () => { + params.element.classList.remove('dscAppWrapper'); + unmount(); + appUnMounted(); + }; + }, + }); + + plugins.urlForwarding.forwardApp('doc', 'discoverLegacy', (path) => { + return `#${path}`; + }); + plugins.urlForwarding.forwardApp('context', 'discoverLegacy', (path) => { + const urlParts = path.split('/'); + // take care of urls containing legacy url, those split in the following way + // ["", "context", indexPatternId, _type, id + params] + if (urlParts[4]) { + // remove _type part + const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/'); + return `#${newPath}`; + } + return `#${path}`; + }); + plugins.urlForwarding.forwardApp('discover', 'discoverLegacy', (path) => { + const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || []; + if (!id) { + return `#${path.replace('/discover', '') || '/'}`; + } + return `#/view/${id}${tail || ''}`; + }); + + this.registerEmbeddable(core, plugins); + + return { + docViews: { + addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), + }, + docViewsLinks: { + addDocViewLink: this.docViewsLinksRegistry.addDocViewLink.bind(this.docViewsLinksRegistry), + }, + }; + } + + start(core: CoreStart, plugins: DiscoverStartPlugins) { + // we need to register the application service at setup, but to render it + // there are some start dependencies necessary, for this reason + // initializeInnerAngular + initializeServices are assigned at start and used + // when the application/embeddable is mounted + this.initializeInnerAngular = async () => { + if (this.innerAngularInitialized) { + return; + } + // this is used by application mount and tests + const { getInnerAngularModule } = await import('./get_inner_angular'); + const module = getInnerAngularModule( + innerAngularName, + core, + plugins, + this.initializerContext + ); + setAngularModule(module); + this.innerAngularInitialized = true; + }; + + setUiActions(plugins.uiActions); + + this.initializeServices = async () => { + if (this.servicesInitialized) { + return { core, plugins }; + } + const services = await buildServices( + core, + plugins, + this.initializerContext, + this.getEmbeddableInjector + ); + setServices(services); + this.servicesInitialized = true; + + return { core, plugins }; + }; + + return { + urlGenerator: this.urlGenerator, + savedSearchLoader: createSavedSearchesLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + search: plugins.data.search, + chrome: core.chrome, + overlays: core.overlays, + }), + }; + } + + stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } + + /** + * register embeddable with a slimmer embeddable version of inner angular + */ + private registerEmbeddable(core: CoreSetup, plugins: DiscoverSetupPlugins) { + if (!this.getEmbeddableInjector) { + throw Error('Discover plugin method getEmbeddableInjector is undefined'); + } + + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + executeTriggerActions: deps.uiActions.executeTriggerActions, + isEditable: () => coreStart.application.capabilities.discover.save as boolean, + }; + }; + + const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); + plugins.embeddable.registerEmbeddableFactory(factory.type, factory); + } + + private getEmbeddableInjector = async () => { + if (!this.embeddableInjector) { + if (!this.initializeServices) { + throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined'); + } + const { core, plugins } = await this.initializeServices(); + getServices().opensearchDashboardsLegacy.loadFontAwesome(); + const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular'); + getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins); + const mountpoint = document.createElement('div'); + this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]); + } + + return this.embeddableInjector; + }; +} diff --git a/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts b/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts new file mode 100644 index 000000000000..55cd59104ecb --- /dev/null +++ b/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + createSavedObjectClass, + SavedObject, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; + +export function createSavedSearchClass(services: SavedObjectOpenSearchDashboardsServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedSearch extends SavedObjectClass { + public static type: string = 'search'; + public static mapping = { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = true; + + public id: string; + public showInRecentlyAccessed: boolean; + + constructor(id: string) { + super({ + id, + type: 'search', + mapping: { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }, + searchSource: true, + defaults: { + title: '', + description: '', + columns: [], + hits: 0, + sort: [], + version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.id = id; + this.getFullPath = () => `/app/discover#/view/${String(id)}`; + } + } + + return SavedSearch as new (id: string) => SavedObject; +} diff --git a/src/plugins/discover_legacy/public/saved_searches/index.ts b/src/plugins/discover_legacy/public/saved_searches/index.ts new file mode 100644 index 000000000000..f576a9a9377a --- /dev/null +++ b/src/plugins/discover_legacy/public/saved_searches/index.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createSavedSearchesLoader } from './saved_searches'; +export { SavedSearch, SavedSearchLoader } from './types'; diff --git a/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts b/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts new file mode 100644 index 000000000000..dd3243568159 --- /dev/null +++ b/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + SavedObjectLoader, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; +import { createSavedSearchClass } from './_saved_search'; + +export function createSavedSearchesLoader(services: SavedObjectOpenSearchDashboardsServices) { + const SavedSearchClass = createSavedSearchClass(services); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); + // Customize loader properties since adding an 's' on type doesn't work for type 'search' . + savedSearchLoader.loaderProperties = { + name: 'searches', + noun: 'Saved Search', + nouns: 'saved searches', + }; + + savedSearchLoader.urlFor = (id: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); + + return savedSearchLoader; +} diff --git a/src/plugins/discover_legacy/public/saved_searches/types.ts b/src/plugins/discover_legacy/public/saved_searches/types.ts new file mode 100644 index 000000000000..e02fd65e6899 --- /dev/null +++ b/src/plugins/discover_legacy/public/saved_searches/types.ts @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISearchSource } from '../../../data/public'; + +export type SortOrder = [string, string]; +export interface SavedSearch { + readonly id: string; + title: string; + searchSource: ISearchSource; + description?: string; + columns: string[]; + sort: SortOrder[]; + destroy: () => void; + lastSavedTitle?: string; +} +export interface SavedSearchLoader { + get: (id: string) => Promise; + urlFor: (id: string) => string; +} diff --git a/src/plugins/discover_legacy/public/url_generator.test.ts b/src/plugins/discover_legacy/public/url_generator.test.ts new file mode 100644 index 000000000000..c352dd5133a4 --- /dev/null +++ b/src/plugins/discover_legacy/public/url_generator.test.ts @@ -0,0 +1,269 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DiscoverUrlGenerator } from './url_generator'; +import { hashedItemStore, getStatesFromOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { mockStorage } from '../../opensearch_dashboards_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; + +const appBasePath: string = 'xyz/app/discover'; +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const generator = new DiscoverUrlGenerator({ + appBasePath, + useHash, + }); + + return { + generator, + }; +}; + +beforeEach(() => { + // @ts-ignore + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({}); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(url.startsWith(appBasePath)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ savedSearchId }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: true, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { generator } = await setup({ useHash: true }); + const url = await generator.createUrl({ + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { generator } = await setup(); + const url = await generator.createUrl({ + useHash: false, + indexPatternId, + }); + + expect(url.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover_legacy/public/url_generator.ts b/src/plugins/discover_legacy/public/url_generator.ts new file mode 100644 index 000000000000..25e8517c8c9d --- /dev/null +++ b/src/plugins/discover_legacy/public/url_generator.ts @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + TimeRange, + Filter, + Query, + opensearchFilters, + QueryState, + RefreshInterval, +} from '../../data/public'; +import { setStateToOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { UrlGeneratorsDefinition } from '../../share/public'; + +export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR'; + +export interface DiscoverUrlGeneratorState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + + /** + * Optionally apply filers. + */ + filters?: Filter[]; + + /** + * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the + * saved dashboard has a query saved with it, this will _replace_ that query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class DiscoverUrlGenerator + implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = DISCOVER_APP_URL_GENERATOR; + + public readonly createUrl = async ({ + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + useHash = this.params.useHash, + }: DiscoverUrlGeneratorState): Promise => { + const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !opensearchFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => opensearchFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let url = `${this.params.appBasePath}#/${savedSearchPath}`; + url = setStateToOsdUrl('_g', queryState, { useHash }, url); + url = setStateToOsdUrl('_a', appState, { useHash }, url); + + return url; + }; +} diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 3865e9852882..f541456c93b4 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -45,6 +45,8 @@ export interface TopNavMenuData { emphasize?: boolean; iconType?: EuiIconType; iconSide?: EuiButtonProps['iconSide']; + // @deprecated - experimental, do not use yet. Will be removed in a future minor version + type?: 'toggle' | 'button'; } export interface RegisteredTopNavMenuData extends TopNavMenuData { diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 7f987d937b96..bbc6ff1a7165 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -30,7 +30,7 @@ import { upperFirst, isFunction } from 'lodash'; import React, { MouseEvent } from 'react'; -import { EuiToolTip, EuiButton, EuiHeaderLink } from '@elastic/eui'; +import { EuiToolTip, EuiButton, EuiHeaderLink, EuiSwitch } from '@elastic/eui'; import { TopNavMenuData } from './top_nav_menu_data'; export function TopNavMenuItem(props: TopNavMenuData) { @@ -58,21 +58,36 @@ export function TopNavMenuItem(props: TopNavMenuData) { className: props.className, }; - const btn = props.emphasize ? ( - - {upperFirst(props.label || props.id!)} - - ) : ( - - {upperFirst(props.label || props.id!)} - - ); + let component; + if (props.type === 'toggle') { + component = ( + { + handleClick((e as unknown) as MouseEvent); + }} + data-test-subj={props.testId} + className={props.className} + /> + ); + } else { + component = props.emphasize ? ( + + {upperFirst(props.label || props.id!)} + + ) : ( + + {upperFirst(props.label || props.id!)} + + ); + } const tooltip = getTooltip(); if (tooltip) { - return {btn}; + return {component}; } - return btn; + return component; } TopNavMenuItem.defaultProps = { diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts index 93a7fff7f2bc..7f9744119c73 100644 --- a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts @@ -43,11 +43,8 @@ describe('shortUrlAssertValid()', () => { ['hostname', 'localhost/app/opensearch-dashboards', PATH_ERROR], // according to spec, this is not a valid URL -- you cannot specify a hostname without a protocol ['hostname and port', 'local.host:5601/app/opensearch-dashboards', PROTOCOL_ERROR], // parser detects 'local.host' as the protocol ['hostname and auth', 'user:pass@localhost.net/app/opensearch-dashboards', PROTOCOL_ERROR], // parser detects 'user' as the protocol - ['path traversal', '/app/../../not-opensearch-dashboards', PATH_ERROR], // fails because there are >2 path parts ['path traversal', '/../not-opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app' - ['deep path', '/app/opensearch-dashboards/foo', PATH_ERROR], // fails because there are >2 path parts - ['deeper path', '/app/opensearch-dashboards/foo/bar', PATH_ERROR], // fails because there are >2 path parts - ['base path', '/base/app/opensearch-dashboards', PATH_ERROR], // fails because there are >2 path parts + ['base path', '/base/app/opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app' ['path with an extra leading slash', '//foo/app/opensearch-dashboards', HOSTNAME_ERROR], // parser detects 'foo' as the hostname ['path with an extra leading slash', '///app/opensearch-dashboards', HOSTNAME_ERROR], // parser detects '' as the hostname ['path without app', '/foo/opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app' @@ -63,10 +60,13 @@ describe('shortUrlAssertValid()', () => { const valid = [ '/app/opensearch-dashboards', '/app/opensearch-dashboards/', // leading and trailing slashes are trimmed + '/app/opensearch-dashboards/deeper', '/app/monitoring#angular/route', '/app/text#document-id', + '/app/text/deeper#document-id', '/app/some?with=query', '/app/some?with=query#and-a-hash', + '/app/some/deeper?with=query#and-a-hash', ]; valid.forEach((url) => { diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts index aec6e743c0bc..b6514a68e6f6 100644 --- a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts @@ -48,7 +48,7 @@ export function shortUrlAssertValid(url: string) { } const pathnameParts = trim(pathname === null ? undefined : pathname, '/').split('/'); - if (pathnameParts.length !== 2 || pathnameParts[0] !== 'app' || !pathnameParts[1]) { + if (pathnameParts[0] !== 'app' || !pathnameParts[1]) { throw Boom.notAcceptable( `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` ); diff --git a/src/test_utils/public/helpers/find_test_subject.ts b/src/test_utils/public/helpers/find_test_subject.ts index 98687e3f0eef..ccb17b336059 100644 --- a/src/test_utils/public/helpers/find_test_subject.ts +++ b/src/test_utils/public/helpers/find_test_subject.ts @@ -54,8 +54,8 @@ const MATCHERS: Matcher[] = [ * @param testSubjectSelector The data test subject selector * @param matcher optional matcher */ -export const findTestSubject = ( - reactWrapper: ReactWrapper, +export const findTestSubject = ( + reactWrapper: ReactWrapper, testSubjectSelector: T, matcher: Matcher = '~=' ) => { diff --git a/src/test_utils/public/testing_lib_helpers.tsx b/src/test_utils/public/testing_lib_helpers.tsx new file mode 100644 index 000000000000..1e39a0cdcecc --- /dev/null +++ b/src/test_utils/public/testing_lib_helpers.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactElement } from 'react'; +import { render as rtlRender } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; + +// src: https://testing-library.com/docs/example-react-intl/#creating-a-custom-render-function +function render(ui: ReactElement, { ...renderOptions } = {}) { + const Wrapper: React.FC = ({ children }) => { + return {children}; + }; + return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); +} + +// re-export everything +export * from '@testing-library/react'; + +// override render method +export { render }; diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js index e612c8d3c41d..23350c81b18f 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.js @@ -52,6 +52,7 @@ export default function ({ getService, getPageObjects }) { await opensearchDashboardsServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, + 'discover:v2': false, }); }); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 2c6bef3a366b..52864a0d7ea3 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -52,6 +52,7 @@ export default function ({ getService, getPageObjects }) { await opensearchDashboardsServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, + 'discover:v2': false, }); }); diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js index a20e6a6a40ca..17219a836230 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.js @@ -38,11 +38,17 @@ export default function ({ getService, getPageObjects }) { const docTable = getService('docTable'); const filterBar = getService('filterBar'); const retry = getService('retry'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'context']); describe('context filters', function contextSize() { beforeEach(async function () { + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); + await browser.refresh(); await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID, { columns: TEST_COLUMN_NAMES, }); diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index a5c3c94474ea..07fbfe00ac2b 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects, loadTestFile }) { await browser.setWindowSize(1200, 800); await opensearchArchiver.loadIfNeeded('logstash_functional'); await opensearchArchiver.load('visualize'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); }); diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index 5beca47f9cae..e23a2caba0c7 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -43,6 +43,7 @@ export default function ({ getService, getPageObjects }) { 'tileMap', 'visChart', 'timePicker', + 'common', ]); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -52,11 +53,17 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); describe('dashboard state', function describeIndexTests() { before(async function () { await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); + + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); + await browser.refresh(); }); after(async function () { @@ -88,6 +95,8 @@ export default function ({ getService, getPageObjects }) { expect(colorChoiceRetained).to.be(true); }); + // the following three tests are skipped because of save search save window bug: + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4698 it('Saved search with no changes will update when the saved object changes', async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); @@ -107,6 +116,9 @@ export default function ({ getService, getPageObjects }) { expect(inViewMode).to.be(true); await PageObjects.header.clickDiscover(); + // Add load save search here since discover link won't take it to the save search link for + // the legacy discover plugin + await PageObjects.discover.loadSavedSearch('my search'); await PageObjects.discover.clickFieldListItemAdd('agent'); await PageObjects.discover.saveSearch('my search'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -126,6 +138,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.saveDashboard('Has local edits'); await PageObjects.header.clickDiscover(); + // Add load save search here since discover link won't take it to the save search link for + // the legacy discover plugin + await PageObjects.discover.loadSavedSearch('my search'); await PageObjects.discover.clickFieldListItemAdd('clientip'); await PageObjects.discover.saveSearch('my search'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js index c1f4e50e6a65..5c97872cdb7e 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.js +++ b/test/functional/apps/dashboard/dashboard_time_picker.js @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.clickNewDashboard(); log.debug('Clicked new dashboard'); await dashboardVisualizations.createAndAddSavedSearch({ - name: 'saved search', + name: 'saved search 1', fields: ['bytes', 'agent'], }); log.debug('added saved search'); diff --git a/test/functional/apps/dashboard/panel_context_menu.ts b/test/functional/apps/dashboard/panel_context_menu.ts index 2d00c81581e6..07af447d4e15 100644 --- a/test/functional/apps/dashboard/panel_context_menu.ts +++ b/test/functional/apps/dashboard/panel_context_menu.ts @@ -38,12 +38,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardVisualizations = getService('dashboardVisualizations'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + const listingTable = getService('listingTable'); const PageObjects = getPageObjects([ 'dashboard', 'header', 'visualize', 'discover', 'timePicker', + 'common', ]); const dashboardName = 'Dashboard Panel Controls Test'; @@ -114,6 +117,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(0); + // need to find the correct save + await PageObjects.dashboard.saveDashboard(dashboardName); }); }); @@ -121,11 +126,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const searchName = 'my search'; before(async () => { + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); + await browser.refresh(); await PageObjects.header.clickDiscover(); await PageObjects.discover.clickNewSearchButton(); await dashboardVisualizations.createSavedSearch({ name: searchName, fields: ['bytes'] }); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.clickDashboard(); + // Have to add steps to actually click on the dashboard; since added browser.refresh() will make + // clickDashboard() to only land on the dashboard listing page + // We need to add browser.refresh() so clickDiscover() lands correctly on the legacy discover page + await listingTable.clickItemLink('dashboard', dashboardName); + await PageObjects.header.waitUntilLoadingHasFinished(); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) await PageObjects.dashboard.switchToEditMode(); diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 721a5de07871..e96d507087fe 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }) { describe('date_nanos', function () { before(async function () { await opensearchArchiver.loadIfNeeded('date_nanos'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'date-nanos' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'date-nanos', + 'discover:v2': false, + }); await security.testUser.setRoles([ 'opensearch_dashboards_admin', 'opensearch_dashboards_date_nanos', diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js index 8578572dfbc5..05b94d3d1d67 100644 --- a/test/functional/apps/discover/_date_nanos_mixed.js +++ b/test/functional/apps/discover/_date_nanos_mixed.js @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }) { describe('date_nanos_mixed', function () { before(async function () { await opensearchArchiver.loadIfNeeded('date_nanos_mixed'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'timestamp-*' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'timestamp-*', + 'discover:v2': false, + }); await security.testUser.setRoles([ 'opensearch_dashboards_admin', 'opensearch_dashboards_date_nanos_mixed', diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 66555ddd0851..d132454a090e 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -41,16 +41,17 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', + 'discover:v2': false, }; describe('discover app', function describeIndexTests() { before(async function () { - // delete .kibana index and update configDoc - await opensearchDashboardsServer.uiSettings.replace(defaultSettings); - log.debug('load opensearch-dashboards index with default index pattern'); await opensearchArchiver.load('discover'); + // delete .kibana index and update configDoc + await opensearchDashboardsServer.uiSettings.replace(defaultSettings); + // and load a set of makelogs data await opensearchArchiver.loadIfNeeded('logstash_functional'); log.debug('discover'); @@ -258,7 +259,10 @@ export default function ({ getService, getPageObjects }) { }); }); it('should show bars in the correct time zone after switching', async function () { - await opensearchDashboardsServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); + await opensearchDashboardsServer.uiSettings.replace({ + 'dateFormat:tz': 'America/Phoenix', + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitOpenSearchDashboardsChrome(); await queryBar.clearQuery(); @@ -273,7 +277,10 @@ export default function ({ getService, getPageObjects }) { }); describe('usage of discover:searchOnPageLoad', () => { it('should fetch data from OpenSearch initially when discover:searchOnPageLoad is false', async function () { - await opensearchDashboardsServer.uiSettings.replace({ 'discover:searchOnPageLoad': false }); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:searchOnPageLoad': false, + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitOpenSearchDashboardsChrome(); @@ -281,7 +288,10 @@ export default function ({ getService, getPageObjects }) { }); it('should not fetch data from OpenSearch initially when discover:searchOnPageLoad is true', async function () { - await opensearchDashboardsServer.uiSettings.replace({ 'discover:searchOnPageLoad': true }); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:searchOnPageLoad': true, + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitOpenSearchDashboardsChrome(); @@ -306,6 +316,7 @@ export default function ({ getService, getPageObjects }) { it('should update the histogram timerange when the query is resubmitted', async function () { await opensearchDashboardsServer.uiSettings.update({ 'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}', + 'discover:v2': false, }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitOpenSearchDashboardsChrome(); diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 391fa97e00a9..f32a85add6fa 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -40,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'long-window-logstash-*', 'dateFormat:tz': 'Europe/Berlin', + 'discover:v2': false, }; describe('discover histogram', function describeIndexTests() { diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index bc0afcb92250..6978c363689b 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const retry = getService('retry'); describe('doc link in discover', function contextSize() { @@ -45,6 +46,9 @@ export default function ({ getService, getPageObjects }) { await opensearchArchiver.loadIfNeeded('discover'); await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.discover.waitForDocTableLoadingComplete(); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index ed7e30201cf2..166aa954c364 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -40,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', + 'discover:v2': false, }; describe('discover doc table', function describeIndexTests() { diff --git a/test/functional/apps/discover/_errors.ts b/test/functional/apps/discover/_errors.ts index f2df3714e230..3251b9215e1a 100644 --- a/test/functional/apps/discover/_errors.ts +++ b/test/functional/apps/discover/_errors.ts @@ -33,6 +33,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const toasts = getService('toasts'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -40,6 +41,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { await opensearchArchiver.loadIfNeeded('logstash_functional'); await opensearchArchiver.load('invalid_scripted_field'); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index a9c4ac34f735..90de964dc4ec 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -46,6 +46,7 @@ export default function ({ getService, getPageObjects }) { await opensearchArchiver.load('discover'); await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'logstash-*', + 'discover:v2': false, }); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index ecefc610b746..50ecb54d27ff 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -41,6 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']); const defaultSettings = { defaultIndex: 'logstash-*', + 'discover:v2': false, }; describe('discover field visualize button', function () { diff --git a/test/functional/apps/discover/_filter_editor.js b/test/functional/apps/discover/_filter_editor.js index 482376994d79..7692d7e6148b 100644 --- a/test/functional/apps/discover/_filter_editor.js +++ b/test/functional/apps/discover/_filter_editor.js @@ -39,6 +39,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', + 'discover:v2': false, }; describe('discover filter editor', function describeIndexTests() { diff --git a/test/functional/apps/discover/_indexpattern_with_encoded_id.ts b/test/functional/apps/discover/_indexpattern_with_encoded_id.ts index 4ff77a744c20..f2cb85fb9280 100644 --- a/test/functional/apps/discover/_indexpattern_with_encoded_id.ts +++ b/test/functional/apps/discover/_indexpattern_with_encoded_id.ts @@ -16,7 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('indexpattern with encoded id', () => { before(async () => { await opensearchArchiver.loadIfNeeded('index_pattern_with_encoded_id'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'with-encoded-id' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'with-encoded-id', + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); }); diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index 6aa50ec2b7df..1f89753c0336 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -43,7 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'opensearch_dashboards_timefield', ]); await opensearchArchiver.loadIfNeeded('index_pattern_without_timefield'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'without-timefield' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'without-timefield', + 'discover:v2': false, + }); await PageObjects.common.navigateToApp('discover'); }); diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js index 5c4262118fa0..5f4c2438fce1 100644 --- a/test/functional/apps/discover/_inspector.js +++ b/test/functional/apps/discover/_inspector.js @@ -53,6 +53,7 @@ export default function ({ getService, getPageObjects }) { // delete .kibana index and update configDoc await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'logstash-*', + 'discover:v2': false, }); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_large_string.js b/test/functional/apps/discover/_large_string.js index 6b0395aa02fd..2cad2b0daf90 100644 --- a/test/functional/apps/discover/_large_string.js +++ b/test/functional/apps/discover/_large_string.js @@ -47,7 +47,10 @@ export default function ({ getService, getPageObjects }) { ]); await opensearchArchiver.load('empty_opensearch_dashboards'); await opensearchArchiver.loadIfNeeded('hamlet'); - await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'testlargestring' }); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'testlargestring', + 'discover:v2': false, + }); }); it('verify the large string book present', async function () { diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 6ba2002653af..c51850eac00d 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -35,11 +35,12 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const opensearchArchiver = getService('opensearchArchiver'); const opensearchDashboardsServer = getService('opensearchDashboardsServer'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']); const browser = getService('browser'); const defaultSettings = { defaultIndex: 'logstash-*', + 'discover:v2': false, }; const filterBar = getService('filterBar'); const queryBar = getService('queryBar'); @@ -54,6 +55,7 @@ export default function ({ getService, getPageObjects }) { // and load a set of makelogs data await opensearchArchiver.loadIfNeeded('logstash_functional'); await opensearchDashboardsServer.uiSettings.replace(defaultSettings); + log.debug('discover'); await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 96b7f705c73a..d18481011560 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -62,6 +62,7 @@ export default function ({ getService, getPageObjects }) { await opensearchDashboardsServer.uiSettings.replace({ 'state:storeInSessionStorage': storeStateInSessionStorage, + 'discover:v2': false, }); log.debug('discover'); @@ -96,7 +97,7 @@ export default function ({ getService, getPageObjects }) { it('should allow for copying the snapshot URL', async function () { const expectedUrl = baseUrl + - '/app/discover?_t=1453775307251#' + + '/app/discoverLegacy?_t=1453775307251#' + '/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' + ":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" + "-23T18:31:44.000Z'))&_a=(columns:!(_source),filters:!(),index:'logstash-" + @@ -121,7 +122,7 @@ export default function ({ getService, getPageObjects }) { it('should allow for copying the saved object URL', async function () { const expectedUrl = baseUrl + - '/app/discover#' + + '/app/discoverLegacy#' + '/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' + '?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)' + "%2Ctime%3A(from%3A'2015-09-19T06%3A31%3A44.000Z'%2C" + @@ -160,7 +161,7 @@ export default function ({ getService, getPageObjects }) { await browser.get(actualUrl, false); await retry.waitFor('shortUrl resolves and opens', async () => { const resolvedUrl = await browser.getCurrentUrl(); - expect(resolvedUrl).to.match(/discover/); + expect(resolvedUrl).to.match(/discoverLegacy/); const resolvedTime = await PageObjects.timePicker.getTimeConfig(); expect(resolvedTime.start).to.equal(actualTime.start); expect(resolvedTime.end).to.equal(actualTime.end); @@ -175,7 +176,7 @@ export default function ({ getService, getPageObjects }) { await browser.get(currentUrl, false); await retry.waitFor('discover to open', async () => { const resolvedUrl = await browser.getCurrentUrl(); - expect(resolvedUrl).to.match(/discover/); + expect(resolvedUrl).to.match(/discoverLegacy/); const { message } = await toasts.getErrorToast(); expect(message).to.contain( 'Unable to completely restore the URL, be sure to use the share functionality.' diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js index 446cb1b760e0..5d6bcb5134e6 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.js @@ -38,17 +38,18 @@ export default function ({ getService, getPageObjects }) { describe('discover sidebar', function describeIndexTests() { before(async function () { - // delete .kibana index and update configDoc - await opensearchDashboardsServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - log.debug('load opensearch-dashboards index with default index pattern'); await opensearchArchiver.load('discover'); // and load a set of makelogs data await opensearchArchiver.loadIfNeeded('logstash_functional'); + // delete .kibana index and update configDoc + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + 'discover:v2': false, + }); + log.debug('discover'); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_source_filters.js b/test/functional/apps/discover/_source_filters.js index d324a0972b8a..1f425a4e341f 100644 --- a/test/functional/apps/discover/_source_filters.js +++ b/test/functional/apps/discover/_source_filters.js @@ -38,17 +38,18 @@ export default function ({ getService, getPageObjects }) { describe('source filters', function describeIndexTests() { before(async function () { - // delete .kibana index and update configDoc - await opensearchDashboardsServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - }); - log.debug('load opensearch-dashboards index with default index pattern'); await opensearchArchiver.load('visualize_source-filters'); // and load a set of makelogs data await opensearchArchiver.loadIfNeeded('logstash_functional'); + // delete .kibana index and update configDoc + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + 'discover:v2': false, + }); + log.debug('discover'); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 03230f1270ed..36f8e50ea543 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -36,11 +36,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); const appsMenu = getService('appsMenu'); const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); describe('OpenSearch Dashboards browser back navigation should work', function describeIndexTests() { before(async () => { await opensearchArchiver.loadIfNeeded('discover'); await opensearchArchiver.loadIfNeeded('logstash_functional'); + + await opensearchDashboardsServer.uiSettings.replace({ 'discover:v2': false }); }); it('detect navigate back issues', async () => { diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index c5c6456f0442..203345e38f91 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -32,6 +32,7 @@ import expect from '@osd/expect'; export default function ({ getService, getPageObjects }) { const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const opensearch = getService('legacyOpenSearch'); const retry = getService('retry'); const security = getService('security'); @@ -42,6 +43,9 @@ export default function ({ getService, getPageObjects }) { await security.testUser.setRoles(['opensearch_dashboards_admin', 'test_alias_reader']); await opensearchArchiver.loadIfNeeded('alias'); await opensearchArchiver.load('empty_opensearch_dashboards'); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); await opensearch.indices.updateAliases({ body: { actions: [ diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 51f0dc44d771..0a6df08a998e 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -71,8 +71,9 @@ export default function ({ getService, getPageObjects }) { before(async function () { await browser.setWindowSize(1200, 800); await opensearchArchiver.load('discover'); - // delete .kibana index and then wait for OpenSearch Dashboards to re-create it - await opensearchDashboardsServer.uiSettings.replace({}); + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); await opensearchDashboardsServer.uiSettings.update({}); }); diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js index 82ecbcb2a655..d852ac484eaa 100644 --- a/test/functional/apps/visualize/_lab_mode.js +++ b/test/functional/apps/visualize/_lab_mode.js @@ -34,8 +34,14 @@ import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualiza export default function ({ getService, getPageObjects }) { const log = getService('log'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); describe('visualize lab mode', () => { + before(async () => { + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); + }); it('disabling does not break loading saved searches', async () => { await PageObjects.common.navigateToUrl('discover', '', { useActualUrl: true }); await PageObjects.discover.saveSearch('visualize_lab_mode_test'); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index fb7e721db7de..2bdc5990b928 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -49,6 +49,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + 'discover:v2': false, }); isOss = await deployment.isOss(); }); diff --git a/test/functional/config.js b/test/functional/config.js index d927aea2966f..b862208276bf 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -80,7 +80,7 @@ export default async function ({ readConfigFile }) { pathname: '/status', }, discover: { - pathname: '/app/discover', + pathname: '/app/discoverLegacy', hash: '/', }, context: { diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index aee71c8d58ba..588368d4b4a1 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -37,6 +37,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F const queryBar = getService('queryBar'); const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); + const browser = getService('browser'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); const PageObjects = getPageObjects([ 'dashboard', 'visualize', @@ -44,6 +46,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F 'header', 'discover', 'timePicker', + 'common', + 'settings', ]); return new (class DashboardVisualizations { @@ -69,6 +73,11 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F fields?: string[]; }) { log.debug(`createSavedSearch(${name})`); + + await opensearchDashboardsServer.uiSettings.replace({ + 'discover:v2': false, + }); + await browser.refresh(); await PageObjects.header.clickDiscover(); await PageObjects.timePicker.setHistoricalDataRange();