From d72e632d797c8f53004518514aaad630564b596b Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Mon, 13 Dec 2021 14:32:25 -0800 Subject: [PATCH 01/16] Update tests, builds and doc (#318) * rebased with bwc tests Signed-off-by: Shenoy Pratik * updated bwc tests Signed-off-by: Shenoy Pratik * added release notes Signed-off-by: Shenoy Pratik --- ...-observability-test-and-build-workflow.yml | 2 +- opensearch-observability/build.gradle | 23 ++++++++++++++++--- ...h-trace-analytics.release-notes-1.2.1.0.md | 6 +++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md diff --git a/.github/workflows/opensearch-observability-test-and-build-workflow.yml b/.github/workflows/opensearch-observability-test-and-build-workflow.yml index 1ba57de04..307df7332 100644 --- a/.github/workflows/opensearch-observability-test-and-build-workflow.yml +++ b/.github/workflows/opensearch-observability-test-and-build-workflow.yml @@ -3,7 +3,7 @@ name: Test and Build OpenSearch Observability Backend Plugin on: [pull_request, push] env: - OPENSEARCH_VERSION: '1.2.0-SNAPSHOT' + OPENSEARCH_VERSION: '1.2.1-SNAPSHOT' OPENSEARCH_BRANCH: '1.2' COMMON_UTILS_BRANCH: 'main' diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 74169b425..906a0aec8 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,7 +9,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.2.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "1.2.1-SNAPSHOT") // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -254,7 +254,6 @@ testClusters.integTest { setting 'path.repo', repo.absolutePath } - String bwcVersion = "1.1.0-SNAPSHOT" String baseName = "obsBwcCluster" String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc/" @@ -263,7 +262,7 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" - versions = ["1.1.0","1.2.0-SNAPSHOT"] + versions = ["1.1.0","1.2.1-SNAPSHOT"] numberOfNodes = 3 plugin(provider(new Callable(){ @Override @@ -395,6 +394,24 @@ task bwcTestSuite(type: StandaloneRestIntegTestTask) { dependsOn tasks.named("${baseName}#fullRestartClusterTask") } +task integTestRemote(type: RestIntegTestTask) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', opensearch_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + + // Only rest case can run with remote cluster + if (System.getProperty("tests.rest.cluster") != null) { + filter { + includeTestsMatching "org.opensearch.observability.rest.*IT" + } + } +} + run { doFirst { // There seems to be an issue when running multi node run or integ tasks with unicast_hosts diff --git a/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md b/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md new file mode 100644 index 000000000..1cb328ba1 --- /dev/null +++ b/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md @@ -0,0 +1,6 @@ +## Version 1.2.1.0 Release Notes + +Compatible with OpenSearch Version 1.2.1 and OpenSearch Dashboards Version 1.2.0 + +### Maintenance +* Bump observability version for OpenSearch 1.2.1 release ([#313](https://github.com/opensearch-project/trace-analytics/pull/313)) \ No newline at end of file From 154de2434c25f1a2c273b0fe63d84339164d91d8 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 14 Dec 2021 11:57:08 -0800 Subject: [PATCH 02/16] Rename trace-analytics to observability (#341) Signed-off-by: Joshua Li --- .github/draft-release-notes-config.yml | 2 +- DEVELOPER_GUIDE.md | 2 +- README.md | 52 +++++++++---------- .../.cypress/utils/constants.js | 14 ++--- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/.github/draft-release-notes-config.yml b/.github/draft-release-notes-config.yml index f552c0ce3..371f1b065 100644 --- a/.github/draft-release-notes-config.yml +++ b/.github/draft-release-notes-config.yml @@ -5,7 +5,7 @@ template: | # Setting the formatting and sorting for the release notes body name-template: Version $RESOLVED_VERSION -change-template: "* $TITLE ([#$NUMBER](https://github.com/opensearch-project/trace-analytics/pull/$NUMBER))" +change-template: "* $TITLE ([#$NUMBER](https://github.com/opensearch-project/observability/pull/$NUMBER))" sort-by: merged_at sort-direction: ascending replacers: diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 3820efcd2..4f6b758a6 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -11,7 +11,7 @@ So you want to contribute code to this project? Excellent! We're glad you're her 1. cd into `plugins` directory in the OpenSearch Dashboards source code directory. 1. Check out this package from version control into the `plugins` directory. ```bash -git clone git@github.com:opensearch-project/trace-analytics.git plugins --no-checkout +git clone git@github.com:opensearch-project/observability.git plugins --no-checkout cd plugins echo 'dashboards-observability/*' >> .git/info/sparse-checkout git config core.sparseCheckout true diff --git a/README.md b/README.md index 7c8a15567..7e2d6ef5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# OpenSearch Dashboards Observability +# Observability -The OpenSearch Dashboards Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. +The Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. ## Code Summary @@ -37,39 +37,35 @@ The OpenSearch Dashboards Observability plugin has four components: Trace Analyt | [![enhancements open][enhancement-badge]][enhancement-link] | | [![bugs open][bug-badge]][bug-link] | -# OpenSearch Dashboards Observability - -The OpenSearch Dashboards Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. - -[dco-badge]: https://github.com/opensearch-project/trace-analytics/actions/workflows/dco.yml/badge.svg -[dco-badge-link]: https://github.com/opensearch-project/trace-analytics/actions/workflows/dco.yml -[link-check-badge]: https://github.com/opensearch-project/trace-analytics/actions/workflows/link-checker.yml/badge.svg -[link-check-link]: https://github.com/opensearch-project/trace-analytics/actions/workflows/link-checker.yml -[dashboard-build-badge]: https://github.com/opensearch-project/trace-analytics/actions/workflows/dashboards-observability-test-and-build-workflow.yml/badge.svg -[dashboard-build-link]: https://github.com/opensearch-project/trace-analytics/actions/workflows/dashboards-observability-test-and-build-workflow.yml -[opensearch-build-badge]: https://github.com/opensearch-project/trace-analytics/actions/workflows/opensearch-observability-test-and-build-workflow.yml/badge.svg -[opensearch-build-link]: https://github.com/opensearch-project/trace-analytics/actions/workflows/opensearch-observability-test-and-build-workflow.yml -[dashboard-codecov-badge]: https://codecov.io/gh/opensearch-project/trace-analytics/branch/main/graphs/badge.svg?flag=dashboards-observability -[opensearch-codecov-badge]: https://codecov.io/gh/opensearch-project/trace-analytics/branch/main/graphs/badge.svg?flag=opensearch-observability -[codecov-link]: https://codecov.io/gh/opensearch-project/trace-analytics +[dco-badge]: https://github.com/opensearch-project/observability/actions/workflows/dco.yml/badge.svg +[dco-badge-link]: https://github.com/opensearch-project/observability/actions/workflows/dco.yml +[link-check-badge]: https://github.com/opensearch-project/observability/actions/workflows/link-checker.yml/badge.svg +[link-check-link]: https://github.com/opensearch-project/observability/actions/workflows/link-checker.yml +[dashboard-build-badge]: https://github.com/opensearch-project/observability/actions/workflows/dashboards-observability-test-and-build-workflow.yml/badge.svg +[dashboard-build-link]: https://github.com/opensearch-project/observability/actions/workflows/dashboards-observability-test-and-build-workflow.yml +[opensearch-build-badge]: https://github.com/opensearch-project/observability/actions/workflows/opensearch-observability-test-and-build-workflow.yml/badge.svg +[opensearch-build-link]: https://github.com/opensearch-project/observability/actions/workflows/opensearch-observability-test-and-build-workflow.yml +[dashboard-codecov-badge]: https://codecov.io/gh/opensearch-project/observability/branch/main/graphs/badge.svg?flag=dashboards-observability +[opensearch-codecov-badge]: https://codecov.io/gh/opensearch-project/observability/branch/main/graphs/badge.svg?flag=opensearch-observability +[codecov-link]: https://codecov.io/gh/opensearch-project/observability [cypress-test-badge]: https://img.shields.io/badge/Cypress%20tests-in%20progress-yellow [cypress-test-link]: https://github.com/opensearch-project/opensearch-build/issues/1124 [cypress-code-badge]: https://img.shields.io/badge/Cypress%20code-blue -[cypress-code-link]: https://github.com/opensearch-project/trace-analytics/blob/main/dashboards-observability/.cypress/CYPRESS_TESTS.md +[cypress-code-link]: https://github.com/opensearch-project/observability/blob/main/dashboards-observability/.cypress/CYPRESS_TESTS.md [opensearch-it-badge]: https://img.shields.io/badge/OpenSearch%20Plugin%20IT%20tests-in%20progress-yellow [opensearch-it-link]: https://github.com/opensearch-project/opensearch-build/issues/1124 [opensearch-it-code-badge]: https://img.shields.io/badge/OpenSearch%20IT%20code-blue -[opensearch-it-code-link]: https://github.com/opensearch-project/trace-analytics/blob/main/opensearch-observability/src/test/kotlin/org/opensearch/observability/ObservabilityPluginIT.kt +[opensearch-it-code-link]: https://github.com/opensearch-project/observability/blob/main/opensearch-observability/src/test/kotlin/org/opensearch/observability/ObservabilityPluginIT.kt [bwc-tests-badge]: https://img.shields.io/badge/BWC%20tests-in%20progress-yellow -[bwc-tests-link]: https://github.com/opensearch-project/trace-analytics/issues/276 -[good-first-badge]: https://img.shields.io/github/issues/opensearch-project/trace-analytics/good%20first%20issue.svg -[good-first-link]: https://github.com/opensearch-project/trace-analytics/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+ -[feature-badge]: https://img.shields.io/github/issues/opensearch-project/trace-analytics/feature.svg -[feature-link]: https://github.com/opensearch-project/trace-analytics/issues?q=is%3Aopen+is%3Aissue+label%3Afeature -[bug-badge]: https://img.shields.io/github/issues/opensearch-project/trace-analytics/bug.svg -[bug-link]: https://github.com/opensearch-project/trace-analytics/issues?q=is%3Aopen+is%3Aissue+label%3Abug+ -[enhancement-badge]: https://img.shields.io/github/issues/opensearch-project/trace-analytics/enhancement.svg -[enhancement-link]: https://github.com/opensearch-project/trace-analytics/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+ +[bwc-tests-link]: https://github.com/opensearch-project/observability/issues/276 +[good-first-badge]: https://img.shields.io/github/issues/opensearch-project/observability/good%20first%20issue.svg +[good-first-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+ +[feature-badge]: https://img.shields.io/github/issues/opensearch-project/observability/feature.svg +[feature-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Afeature +[bug-badge]: https://img.shields.io/github/issues/opensearch-project/observability/bug.svg +[bug-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Abug+ +[enhancement-badge]: https://img.shields.io/github/issues/opensearch-project/observability/enhancement.svg +[enhancement-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+ ### Trace Analytics diff --git a/dashboards-observability/.cypress/utils/constants.js b/dashboards-observability/.cypress/utils/constants.js index fb3afbdc4..dbaaa277a 100644 --- a/dashboards-observability/.cypress/utils/constants.js +++ b/dashboards-observability/.cypress/utils/constants.js @@ -12,18 +12,18 @@ export const SERVICE_NAME = 'frontend-client'; export const testDataSet = [ { - mapping_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-service-map-mappings.json', - data_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-service-map.json', + mapping_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-service-map-mappings.json', + data_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-service-map.json', index: 'otel-v1-apm-service-map', }, { - mapping_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json', - data_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001.json', + mapping_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json', + data_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001.json', index: 'otel-v1-apm-span-000001', }, { - mapping_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json', - data_url: 'https://raw.githubusercontent.com/opensearch-project/trace-analytics/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000002.json', + mapping_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json', + data_url: 'https://raw.githubusercontent.com/opensearch-project/observability/main/dashboards-observability/.cypress/utils/otel-v1-apm-span-000002.json', index: 'otel-v1-apm-span-000002', }, ] @@ -110,4 +110,4 @@ export const TESTING_PANEL = 'Mock Testing Panels'; export const SAVE_QUERY1 = 'Mock Flight Events Overview'; export const SAVE_QUERY2 = 'Mock Flight count by destination'; export const SAVE_QUERY3 = 'Mock Flight count by destination save to panel'; -export const SAVE_QUERY4 = 'Mock Flight peek'; \ No newline at end of file +export const SAVE_QUERY4 = 'Mock Flight peek'; From 352316f1840dcb19f8551528ba9d0d64393676fe Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Tue, 14 Dec 2021 14:47:16 -0800 Subject: [PATCH 03/16] Event analytics unit tests (#342) * tests Signed-off-by: Eric Wei * updated some snapshots Signed-off-by: Eric Wei * resolved few failing tests Signed-off-by: Eric Wei * few modifications Signed-off-by: Eric Wei --- .../__snapshots__/data_grid.test.tsx.snap | 430 + .../__snapshots__/no_results.test.tsx.snap | 211 + .../explorer/__tests__/data_grid.test.tsx | 48 + .../explorer/__tests__/explorer.test.tsx | 54 + .../explorer/__tests__/no_results.test.tsx | 27 + .../__snapshots__/docViewer.test.tsx.snap | 761 ++ .../__snapshots__/docViewerRow.test.tsx.snap | 64 + .../docTable/__tests__/docViewer.test.tsx | 34 + .../docTable/__tests__/docViewerRow.test.tsx | 39 + .../explorer/docTable/docViewRow.tsx | 1 - .../json_code_block.test.tsx.snap | 142 + .../__tests__/json_code_block.test.tsx | 34 + .../__snapshots__/hits_counter.test.tsx.snap | 142 + .../__tests__/hits_counter.test.tsx | 32 + .../public/components/explorer/home.tsx | 4 +- .../saved_query_table.test.tsx.snap | 1686 ++++ .../__tests__/saved_query_table.test.tsx | 35 + ...istory_table.tsx => saved_query_table.tsx} | 6 +- .../__snapshots__/save_panel.test.tsx.snap | 695 ++ .../save_panel/__tests__/save_panel.test.tsx | 40 + .../components/explorer/save_panel/index.ts | 2 +- .../{savePanel.tsx => save_panel.tsx} | 2 - .../__snapshots__/field.test.tsx.snap | 296 + .../__snapshots__/sidebar.test.tsx.snap | 8644 +++++++++++++++++ .../explorer/sidebar/__tests__/field.test.tsx | 41 + .../sidebar/__tests__/sidebar.test.tsx | 93 + .../timechart_header.test.tsx.snap | 333 + .../__tests__/timechart_header.test.tsx | 35 + .../timechart_header/timechart_header.tsx | 26 +- .../__snapshots__/datapanel.test.tsx.snap | 6258 ++++++++++++ .../field_accordion.test.tsx.snap | 5890 +++++++++++ .../__snapshots__/field_item.test.tsx.snap | 288 + .../__snapshots__/field_list.test.tsx.snap | 234 + .../lens_field_icon.test.tsx.snap | 55 + .../__tests__/datapanel.test.tsx | 45 + .../__tests__/field_accordion.test.tsx | 36 + .../__tests__/field_item.test.tsx | 36 + .../__tests__/field_list.test.tsx | 33 + .../__tests__/lens_field_icon.test.tsx | 29 + .../__snapshots__/assets.test.tsx.snap | 64 + .../assets/__tests__/assets.test.tsx | 56 + .../explorer/visualizations/assets/legend.tsx | 38 - .../__snapshots__/config_panel.test.tsx.snap | 3666 +++++++ .../__tests__/config_panel.test.tsx | 81 + .../count_distribution.test.tsx.snap | 481 + .../__tests__/count_distribution.test.tsx | 43 + .../shared_components.test.tsx.snap | 198 + .../__tests__/shared_components.test.tsx | 52 + .../visualizations/shared_components/index.ts | 1 - .../legend_settings_popover.tsx | 158 - .../shared_components/toolbar_popover.tsx | 81 - .../__snapshots__/workspace.test.tsx.snap | 3881 ++++++++ .../__tests__/workspace.test.tsx | 137 + .../workspace_panel/workspace_panel.tsx | 3 - .../__tests__/__snapshots__/bar.test.tsx.snap | 389 + .../horizontal_bar.test.tsx.snap | 567 ++ .../__snapshots__/line.test.tsx.snap | 397 + .../charts/__tests__/bar.test.tsx | 36 + .../charts/__tests__/horizontal_bar.test.tsx | 36 + .../charts/__tests__/line.test.tsx | 36 + .../__snapshots__/plotly.test.tsx.snap | 317 + .../plotly/__tests__/plotly.test.tsx | 69 + .../test/event_analytics_constants.ts | 472 + 63 files changed, 37805 insertions(+), 315 deletions(-) create mode 100644 dashboards-observability/public/components/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/__tests__/__snapshots__/no_results.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/__tests__/data_grid.test.tsx create mode 100644 dashboards-observability/public/components/explorer/__tests__/explorer.test.tsx create mode 100644 dashboards-observability/public/components/explorer/__tests__/no_results.test.tsx create mode 100644 dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewer.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewerRow.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/docTable/__tests__/docViewer.test.tsx create mode 100644 dashboards-observability/public/components/explorer/docTable/__tests__/docViewerRow.test.tsx create mode 100644 dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/__snapshots__/json_code_block.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/json_code_block.test.tsx create mode 100644 dashboards-observability/public/components/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/hits_counter/__tests__/hits_counter.test.tsx create mode 100644 dashboards-observability/public/components/explorer/home_table/__tests__/__snapshots__/saved_query_table.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/home_table/__tests__/saved_query_table.test.tsx rename dashboards-observability/public/components/explorer/home_table/{history_table.tsx => saved_query_table.tsx} (97%) create mode 100644 dashboards-observability/public/components/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/save_panel/__tests__/save_panel.test.tsx rename dashboards-observability/public/components/explorer/save_panel/{savePanel.tsx => save_panel.tsx} (98%) create mode 100644 dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/sidebar/__tests__/field.test.tsx create mode 100644 dashboards-observability/public/components/explorer/sidebar/__tests__/sidebar.test.tsx create mode 100644 dashboards-observability/public/components/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/timechart_header/__tests__/timechart_header.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/datapanel.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_accordion.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_item.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_list.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/lens_field_icon.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/datapanel.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/field_accordion.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/field_item.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/field_list.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/__tests__/lens_field_icon.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/assets/__tests__/assets.test.tsx delete mode 100644 dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/config_panel.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/count_distribution.test.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/shared_components.test.tsx delete mode 100644 dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx delete mode 100644 dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx create mode 100644 dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/__snapshots__/workspace.test.tsx.snap create mode 100644 dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/workspace.test.tsx create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/bar.test.tsx create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/horizontal_bar.test.tsx create mode 100644 dashboards-observability/public/components/visualizations/charts/__tests__/line.test.tsx create mode 100644 dashboards-observability/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap create mode 100644 dashboards-observability/public/components/visualizations/plotly/__tests__/plotly.test.tsx create mode 100644 dashboards-observability/test/event_analytics_constants.ts diff --git a/dashboards-observability/public/components/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap b/dashboards-observability/public/components/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap new file mode 100644 index 000000000..4fa0f57dc --- /dev/null +++ b/dashboards-observability/public/components/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap @@ -0,0 +1,430 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datagrid component Renders data grid component 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + +
+ + double_per_ip_bytes + + host + + ip_count + + per_ip_bytes + + resp_code + + sum_bytes +
+ +
+ +
+
+`; diff --git a/dashboards-observability/public/components/explorer/__tests__/__snapshots__/no_results.test.tsx.snap b/dashboards-observability/public/components/explorer/__tests__/__snapshots__/no_results.test.tsx.snap new file mode 100644 index 000000000..e7e97a213 --- /dev/null +++ b/dashboards-observability/public/components/explorer/__tests__/__snapshots__/no_results.test.tsx.snap @@ -0,0 +1,211 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`No result component Renders No result component 1`] = ` + + + + + +
+ + +
+ +
+ + } + > +
+
+ + + + No results match your search criteria + + +
+
+
+ +
+ + +
+

+ + Expand your time range or modify your query + +

+

+ + Your query may not match anything in the current time range, or there may not be any data at all in the currently selected time range. Try change time range, query filters or choose different time fields + +

+
+
+
+ +
+ + + + + +`; diff --git a/dashboards-observability/public/components/explorer/__tests__/data_grid.test.tsx b/dashboards-observability/public/components/explorer/__tests__/data_grid.test.tsx new file mode 100644 index 000000000..a45d7ecd9 --- /dev/null +++ b/dashboards-observability/public/components/explorer/__tests__/data_grid.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { DataGrid } from '../data_grid'; +import { + SELECTED_FIELDS, + AVAILABLE_FIELDS, + UNSELECTED_FIELDS, + QUERIED_FIELDS +} from '../../../../common/constants/explorer'; +import { + AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, + QUERY_FIELDS, + DATA_GRID_ROWS +} from '../../../../test/event_analytics_constants'; + +describe('Datagrid component', () => { + configure({ adapter: new Adapter() }); + + it('Renders data grid component', async () => { + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS + }; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/__tests__/explorer.test.tsx b/dashboards-observability/public/components/explorer/__tests__/explorer.test.tsx new file mode 100644 index 000000000..2b196a6df --- /dev/null +++ b/dashboards-observability/public/components/explorer/__tests__/explorer.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import httpClientMock from '../../../../test/__mocks__/httpClientMock'; +import { Explorer } from '../explorer'; +import PPLService from '../../../services/requests/ppl'; +import DSLService from '../../../services/requests/dsl'; +import SavedObjects from '../../../services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from '../../../services/timestamp/timestamp'; +import { coreStartMock } from '../../../../test/__mocks__/coreMocks'; + +describe.skip('Event explorer component', () => { + configure({ adapter: new Adapter() }); + + it('Renders explorer component', async () => { + const pplService = new PPLService(httpClientMock); + const dslService = new DSLService(httpClientMock); + const tabId = 'query-panel-1'; + const savedObjects = new SavedObjects(httpClientMock); + const timestampUtils = new TimestampUtils(dslService); + const setToast = jest.fn(); + const history = jest.fn() as any; + history.replace = jest.fn(); + history.push = jest.fn(); + const notifications = coreStartMock.notifications; + const savedObjectId = 'JIcoln0BYMuJGDsOLTnM'; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/__tests__/no_results.test.tsx b/dashboards-observability/public/components/explorer/__tests__/no_results.test.tsx new file mode 100644 index 000000000..b87c8dc13 --- /dev/null +++ b/dashboards-observability/public/components/explorer/__tests__/no_results.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { NoResults } from '../no_results'; + +describe('No result component', () => { + configure({ adapter: new Adapter() }); + + it('Renders No result component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewer.test.tsx.snap b/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewer.test.tsx.snap new file mode 100644 index 000000000..8b1132246 --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewer.test.tsx.snap @@ -0,0 +1,761 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datagrid Doc viewer component Renders Doc viewer component 1`] = ` + +
+ , + "id": "doc_viewer_tab_1", + "name": "Table", + } + } + tabs={ + Array [ + Object { + "content": , + "id": "doc_viewer_tab_1", + "name": "Table", + }, + Object { + "content": , + "id": "doc_viewer_tab_2", + "name": "JSON", + }, + ] + } + > +
+ +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + } + delay="regular" + position="top" + > + + + + + + + + + + } + delay="regular" + position="top" + > + + + + + + + + + + } + delay="regular" + position="top" + > + + + + + + + + + + +
+ +
+ + + + Carrier + + + +
+
+
+
+
+
+
+
+ + + } + delay="regular" + position="top" + > + + + + + + + + + + } + delay="regular" + position="top" + > + + + + + + + + + + } + delay="regular" + position="top" + > + + + + + + + + + + +
+ +
+ + + + avg(FlightDelayMin) + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewerRow.test.tsx.snap b/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewerRow.test.tsx.snap new file mode 100644 index 000000000..fdeca17de --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/__tests__/__snapshots__/docViewerRow.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datagrid Doc viewer row component Renders Doc viewer row component 1`] = ` + + + + + + + 45.957544288332315 + + + +`; diff --git a/dashboards-observability/public/components/explorer/docTable/__tests__/docViewer.test.tsx b/dashboards-observability/public/components/explorer/docTable/__tests__/docViewer.test.tsx new file mode 100644 index 000000000..3a807d7c9 --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/__tests__/docViewer.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { DocViewer } from '../docViewer'; + +describe('Datagrid Doc viewer component', () => { + configure({ adapter: new Adapter() }); + + it('Renders Doc viewer component', async () => { + + const hit = { + 'Carrier': 'JetBeats', + 'avg(FlightDelayMin)': '45.957544288332315' + }; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/docTable/__tests__/docViewerRow.test.tsx b/dashboards-observability/public/components/explorer/docTable/__tests__/docViewerRow.test.tsx new file mode 100644 index 000000000..684ede48c --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/__tests__/docViewerRow.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { DocViewRow } from '../docViewRow'; + +describe('Datagrid Doc viewer row component', () => { + configure({ adapter: new Adapter() }); + + it('Renders Doc viewer row component', async () => { + + const hit = { + 'Carrier': 'JetBeats', + 'avg(FlightDelayMin)': '45.957544288332315' + }; + const selectedCols = [{ + name: 'avg(FlightDelayMin)', + type: 'double' + }]; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/docTable/docViewRow.tsx b/dashboards-observability/public/components/explorer/docTable/docViewRow.tsx index 833f5fcec..43926b256 100644 --- a/dashboards-observability/public/components/explorer/docTable/docViewRow.tsx +++ b/dashboards-observability/public/components/explorer/docTable/docViewRow.tsx @@ -12,7 +12,6 @@ import { } from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { DocViewer } from './docViewer'; -import { DocDetailTitle } from './detailTable/docDetailTitle'; import { IField } from '../../../../common/types/explorer'; export interface IDocType { diff --git a/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/__snapshots__/json_code_block.test.tsx.snap b/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/__snapshots__/json_code_block.test.tsx.snap new file mode 100644 index 000000000..fd63e4c24 --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/__snapshots__/json_code_block.test.tsx.snap @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Doc viewer JSON block component Renders JSON block component 1`] = ` + + + + + { + "Carrier": "JetBeats", + "avg(FlightDelayMin)": "45.957544288332315" +} +
+ } + > + { + "Carrier": "JetBeats", + "avg(FlightDelayMin)": "45.957544288332315" +} + + +
+
+            
+          
+
+
+ + + + + + + + + + + +
+
+
+
+ + + +`; diff --git a/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/json_code_block.test.tsx b/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/json_code_block.test.tsx new file mode 100644 index 000000000..c79a7611f --- /dev/null +++ b/dashboards-observability/public/components/explorer/docTable/json_code_block/__tests__/json_code_block.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { JsonCodeBlock } from '../json_code_block'; + +describe('Doc viewer JSON block component', () => { + configure({ adapter: new Adapter() }); + + it('Renders JSON block component', async () => { + + const hit = { + 'Carrier': 'JetBeats', + 'avg(FlightDelayMin)': '45.957544288332315' + }; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap b/dashboards-observability/public/components/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap new file mode 100644 index 000000000..13a8ec20c --- /dev/null +++ b/dashboards-observability/public/components/explorer/hits_counter/__tests__/__snapshots__/hits_counter.test.tsx.snap @@ -0,0 +1,142 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Hits counter component Renders hits counter 1`] = ` + + + + + +
+ +
+ +
+ + 815 + + + + hits + +
+
+
+
+
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/hits_counter/__tests__/hits_counter.test.tsx b/dashboards-observability/public/components/explorer/hits_counter/__tests__/hits_counter.test.tsx new file mode 100644 index 000000000..fca4b5fee --- /dev/null +++ b/dashboards-observability/public/components/explorer/hits_counter/__tests__/hits_counter.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { HitsCounter } from '../hits_counter'; + +describe('Hits counter component', () => { + configure({ adapter: new Adapter() }); + + it('Renders hits counter', async () => { + const onResetQuery = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/home.tsx b/dashboards-observability/public/components/explorer/home.tsx index 96c1b1ce9..d2e1ac5b4 100644 --- a/dashboards-observability/public/components/explorer/home.tsx +++ b/dashboards-observability/public/components/explorer/home.tsx @@ -48,7 +48,7 @@ import { addTab, selectQueryTabs } from './slices/query_tab_slice'; import { init as initFields } from './slices/field_slice'; import { init as initQuery, changeQuery } from './slices/query_slice'; import { init as initQueryResult, selectQueryResult } from './slices/query_result_slice'; -import { Histories as EventHomeHistories } from './home_table/history_table'; +import { SavedQueryTable } from './home_table/saved_query_table'; import { selectQueries } from './slices/query_slice'; import { setSelectedQueryTab } from './slices/query_tab_slice'; import { DeletePanelModal } from '../custom_panels/helpers/modal_containers'; @@ -404,7 +404,7 @@ export const Home = (props: IHomeProps) => { {savedHistories.length > 0 ? ( - + +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+ + +
+ + + Type + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="field_value_selection_0" + isOpen={false} + ownFocus={true} + panelClassName="euiFilterGroup__popoverPanel" + panelPaddingSize="none" + withTitle={false} + > + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+
+ + + +
+
+
+ + + Name + + +
+
+
+ + + Type + + +
+
+
+ + +
+ +
+
+ + +
+
+
+
+ + + + + +
+
+
+
+ Name +
+
+ + + +
+
+
+ Type +
+
+ + Visualization + +
+
+
+ + +
+ +
+
+ + +
+
+
+
+ + + + + +
+
+
+
+ Name +
+
+ + + +
+
+
+ Type +
+
+ + Visualization + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} + > + +
+
+ + + +
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ + +`; diff --git a/dashboards-observability/public/components/explorer/home_table/__tests__/saved_query_table.test.tsx b/dashboards-observability/public/components/explorer/home_table/__tests__/saved_query_table.test.tsx new file mode 100644 index 000000000..c8e4eee8b --- /dev/null +++ b/dashboards-observability/public/components/explorer/home_table/__tests__/saved_query_table.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { SavedQueryTable } from '../saved_query_table'; +import { SAVED_HISTORIES } from '../../../../../test/event_analytics_constants'; + +describe('Saved query table component', () => { + configure({ adapter: new Adapter() }); + + it('Renders saved query table', async () => { + const handleHistoryClick = jest.fn(); + const handleSelectHistory = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/home_table/history_table.tsx b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx similarity index 97% rename from dashboards-observability/public/components/explorer/home_table/history_table.tsx rename to dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx index c8912f9f0..50802c13b 100644 --- a/dashboards-observability/public/components/explorer/home_table/history_table.tsx +++ b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx @@ -11,19 +11,19 @@ import { } from '@elastic/eui'; import { FILTER_OPTIONS } from '../../../../common/constants/explorer'; -interface TableData { +interface savedQueryTableProps { savedHistories: Array; handleHistoryClick: (objectId: string) => void; handleSelectHistory: (selectedHistories: Array) => void; isTableLoading: boolean; } -export function Histories({ +export function SavedQueryTable({ savedHistories, handleHistoryClick, handleSelectHistory, isTableLoading -}: TableData) { +}: savedQueryTableProps) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const pageIndexRef = useRef(); diff --git a/dashboards-observability/public/components/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap b/dashboards-observability/public/components/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap new file mode 100644 index 000000000..6344e67ec --- /dev/null +++ b/dashboards-observability/public/components/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap @@ -0,0 +1,695 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Saved query table component Renders saved query table 1`] = ` + + +

+ Custom operational dashboards/application +

+
+ +
+
+ +
+ + +
+
+
+ + + + + + + + [Logs] Web traffic Panel + + + + + + + + + + + + + + + + [Logs] Web traffic Panel 2 + + + + + + + + + +
+ +
+
+ +
+ +
+ + + + + + + + +
+
+
+
+ + +
+ + +
+ Search existing dashboards or applications by name +
+
+
+
+ + +

+ Name +

+
+ +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ Name for your savings +
+
+
+
+
+ +`; diff --git a/dashboards-observability/public/components/explorer/save_panel/__tests__/save_panel.test.tsx b/dashboards-observability/public/components/explorer/save_panel/__tests__/save_panel.test.tsx new file mode 100644 index 000000000..8a907ac2a --- /dev/null +++ b/dashboards-observability/public/components/explorer/save_panel/__tests__/save_panel.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { SavePanel } from '../save_panel'; +import { SELECTED_PANELS_OPTIONS } from '../../../../../test/event_analytics_constants'; +import SavedObjects from '../../../../services/saved_objects/event_analytics/saved_objects'; +import httpClientMock from '../../../../../test/__mocks__/httpClientMock'; + +describe('Saved query table component', () => { + configure({ adapter: new Adapter() }); + + it('Renders saved query table', async () => { + const handleNameChange = jest.fn(); + const handleOptionChange = jest.fn(); + const savedObjects = new SavedObjects(httpClientMock); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/save_panel/index.ts b/dashboards-observability/public/components/explorer/save_panel/index.ts index d78fa44d9..de81603f7 100644 --- a/dashboards-observability/public/components/explorer/save_panel/index.ts +++ b/dashboards-observability/public/components/explorer/save_panel/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { SavePanel } from './savePanel'; \ No newline at end of file +export { SavePanel } from './save_panel'; \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/save_panel/savePanel.tsx b/dashboards-observability/public/components/explorer/save_panel/save_panel.tsx similarity index 98% rename from dashboards-observability/public/components/explorer/save_panel/savePanel.tsx rename to dashboards-observability/public/components/explorer/save_panel/save_panel.tsx index 5aab3e0d9..2d89b5f7a 100644 --- a/dashboards-observability/public/components/explorer/save_panel/savePanel.tsx +++ b/dashboards-observability/public/components/explorer/save_panel/save_panel.tsx @@ -21,7 +21,6 @@ interface ISavedPanelProps { handleNameChange: any; handleOptionChange: any; savedObjects: SavedObjects; - isTextFieldInvalid: boolean; savePanelName: string; showOptionList: boolean; } @@ -38,7 +37,6 @@ export const SavePanel = ({ handleNameChange, handleOptionChange, savedObjects, - isTextFieldInvalid, savePanelName, showOptionList, }: ISavedPanelProps) => { diff --git a/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap b/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap new file mode 100644 index 000000000..44f6114c2 --- /dev/null +++ b/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap @@ -0,0 +1,296 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Field component Renders a sidebar field 1`] = ` + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
+
+ + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + size="s" + > +
+ +
+ + + + + + + + + + +
+
+
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap new file mode 100644 index 000000000..1ee28c193 --- /dev/null +++ b/dashboards-observability/public/components/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -0,0 +1,8644 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Siderbar component Renders empty sidebar component 1`] = ` + + + + +
+ +
+ +
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+`; + +exports[`Siderbar component Renders sidebar component 1`] = ` + + + + +
+ +
+ +
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+ +
+ +
+ +

+ + Query fields + +

+
+ +
+ +
    +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + double_per_ip_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + double_per_ip_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + ip_count + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + ip_count + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + per_ip_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + per_ip_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + resp_code + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + resp_code + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
  • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + sum_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
    +
    + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + sum_bytes + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
  • +
+ +

+ + Selected Fields + +

+
+ +
+ +
    +
    + +

    + + Available Fields + +

    +
    +
    + + + +
    +
    +
      +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + Default Timestamp + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + Default Timestamp + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + Default Timestamp + + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    • + + + + + + Override + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="dscSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > + +
      +
      + + + + + Override + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + size="s" + > +
      + +
      + + + + + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
    • +
    +
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/sidebar/__tests__/field.test.tsx b/dashboards-observability/public/components/explorer/sidebar/__tests__/field.test.tsx new file mode 100644 index 000000000..c6660d934 --- /dev/null +++ b/dashboards-observability/public/components/explorer/sidebar/__tests__/field.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Field } from '../field'; +import { AGENT_FIELD } from '../../../../../test/event_analytics_constants'; + +describe('Field component', () => { + configure({ adapter: new Adapter() }); + + it('Renders a sidebar field', async () => { + const onToggleField = jest.fn(); + const handleOverrideTimestamp = jest.fn(); + const selectedTimestamp = 'timestamp'; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/sidebar/__tests__/sidebar.test.tsx b/dashboards-observability/public/components/explorer/sidebar/__tests__/sidebar.test.tsx new file mode 100644 index 000000000..0e8e23ec4 --- /dev/null +++ b/dashboards-observability/public/components/explorer/sidebar/__tests__/sidebar.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Sidebar } from '../sidebar'; +import { + SELECTED_FIELDS, + AVAILABLE_FIELDS, + UNSELECTED_FIELDS, + QUERIED_FIELDS +} from '../../../../../common/constants/explorer'; +import { + AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, + QUERY_FIELDS, + JSON_DATA, + JSON_DATA_ALL +} from '../../../../../test/event_analytics_constants'; + +describe('Siderbar component', () => { + configure({ adapter: new Adapter() }); + + it('Renders empty sidebar component', async () => { + const explorerFields = { + [SELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [QUERIED_FIELDS]: [] + }; + const handleAddField = jest.fn(); + const handleOverrideTimestamp = jest.fn(); + const selectedTimestamp = 'timestamp'; + const explorerData = {}; + const handleRemoveField = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders sidebar component', async () => { + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS + }; + const handleAddField = jest.fn(); + const handleOverrideTimestamp = jest.fn(); + const selectedTimestamp = 'timestamp'; + const explorerData = { + 'jsonData': JSON_DATA, + 'jsonDataAll': JSON_DATA_ALL + }; + const handleRemoveField = jest.fn(); + + const wrapper = mount( + + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap b/dashboards-observability/public/components/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap new file mode 100644 index 000000000..af8ade96a --- /dev/null +++ b/dashboards-observability/public/components/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap @@ -0,0 +1,333 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Time chart header component Renders Time chart header component 1`] = ` + + + + + +
+ +
+ + + +
+ + + +
+ + +
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + + + + +`; diff --git a/dashboards-observability/public/components/explorer/timechart_header/__tests__/timechart_header.test.tsx b/dashboards-observability/public/components/explorer/timechart_header/__tests__/timechart_header.test.tsx new file mode 100644 index 000000000..138c76bc7 --- /dev/null +++ b/dashboards-observability/public/components/explorer/timechart_header/__tests__/timechart_header.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { TimechartHeader } from '../timechart_header'; +import { TIME_INTERVAL_OPTIONS } from '../../../../../common/constants/explorer'; + +describe('Time chart header component', () => { + configure({ adapter: new Adapter() }); + + it('Renders Time chart header component', async () => { + + const onChangeInterval = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx b/dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx index 1bbd8d2cb..651563023 100644 --- a/dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx +++ b/dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx @@ -14,14 +14,6 @@ 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 */ @@ -44,26 +36,10 @@ export interface TimechartHeaderProps { } export function TimechartHeader({ - bucketInterval, - dateFormat, - timeRange, options, - onChangeInterval, - stateInterval, + onChangeInterval }: TimechartHeaderProps) { const [interval, setInterval] = useState(options[0].value); - const toMoment = useCallback( - (datetime: string) => { - if (!datetime) { - return ''; - } - if (!dateFormat) { - return datetime; - } - return moment(datetime).format(dateFormat); - }, - [dateFormat] - ); const handleIntervalChange = (e: React.ChangeEvent) => { setInterval(e.target.value); diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/datapanel.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/datapanel.test.tsx.snap new file mode 100644 index 000000000..3aa9e30e6 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/datapanel.test.tsx.snap @@ -0,0 +1,6258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data panel component Renders data panel component 1`] = ` + + +
+ +
+ +
+
+ + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+ +
+ +
+ + +
+ +
+
+ + + + Available fields + + + } + extraAction={ + + 20 + + } + id="1" + initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" + > +
+
+ +
+ + + 20 + + +
+
+
+ +
+
+ +
+ +
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+ +
+ +
+ + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_accordion.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_accordion.test.tsx.snap new file mode 100644 index 000000000..3b69e89a7 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_accordion.test.tsx.snap @@ -0,0 +1,5890 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualization fields accordion component Renders fields accordion component 1`] = ` + + + + Available fields + + + } + extraAction={ + + 20 + + } + id="3367" + initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" + > +
+
+ +
+ + + 20 + + +
+
+
+ +
+
+ +
+ +
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + bytes + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + clientip + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + event + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + extension + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + geo + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + host + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + index + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + ip + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + machine + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + memory + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + message + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + phpmemory + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + referer + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + request + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + response + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + tags + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + timestamp + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + url + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+ + + + + } + fieldInfoIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + utc_time + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_item.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_item.test.tsx.snap new file mode 100644 index 000000000..f896951af --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_item.test.tsx.snap @@ -0,0 +1,288 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualization field item component Renders field item component 1`] = ` + + + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + /> + + } + className="lnsFieldItem__popoverAnchor" + closePopover={[Function]} + data-test-subj="lnsFieldListPanelField" + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="lnsFieldItem__fieldPanel" + panelPaddingSize="m" + > + +
+
+ + + + } + fieldInfoIcon={ + + } + fieldName={ + + agent + + } + isActive={false} + onClick={[Function]} + onDragEnd={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDragStart={[Function]} + onDrop={[Function]} + > +
+ +
+
+
+
+
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_list.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_list.test.tsx.snap new file mode 100644 index 000000000..5ffdbb49d --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/field_list.test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualization field list component Renders field list component 1`] = ` + +
+
+ + + + Available fields + + + } + extraAction={ + + 0 + + } + id="3327" + initialIsOpen={false} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" + > +
+
+ +
+ + + 0 + + +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ + +
+
+ +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/lens_field_icon.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/lens_field_icon.test.tsx.snap new file mode 100644 index 000000000..d9cbea0a6 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/__snapshots__/lens_field_icon.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualization field icon component Renders field icon component 1`] = ` + + + + + + + + + + + + + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/datapanel.test.tsx b/dashboards-observability/public/components/explorer/visualizations/__tests__/datapanel.test.tsx new file mode 100644 index 000000000..d6f99c338 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/datapanel.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { DataPanel } from '../datapanel'; +import { + SELECTED_FIELDS, + AVAILABLE_FIELDS, + UNSELECTED_FIELDS, + QUERIED_FIELDS +} from '../../../../../common/constants/explorer'; +import { + AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, + QUERY_FIELDS +} from '../../../../../test/event_analytics_constants'; + +describe('Data panel component', () => { + configure({ adapter: new Adapter() }); + + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS + }; + it('Renders data panel component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/field_accordion.test.tsx b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_accordion.test.tsx new file mode 100644 index 000000000..d171927b3 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_accordion.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { FieldsAccordion } from '../fields_accordion'; +import { + AVAILABLE_FIELDS +} from '../../../../../test/event_analytics_constants'; + +describe('Visualization fields accordion component', () => { + configure({ adapter: new Adapter() }); + + it('Renders fields accordion component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/field_item.test.tsx b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_item.test.tsx new file mode 100644 index 000000000..c5e4cd8f8 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_item.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { FieldItem } from '../field_item'; +import { + AVAILABLE_FIELDS +} from '../../../../../test/event_analytics_constants'; + +describe('Visualization field item component', () => { + configure({ adapter: new Adapter() }); + + it('Renders field item component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/field_list.test.tsx b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_list.test.tsx new file mode 100644 index 000000000..15f97b5c2 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/field_list.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { FieldList } from '../fieldList'; +import { + AVAILABLE_FIELDS +} from '../../../../../test/event_analytics_constants'; + +describe('Visualization field list component', () => { + configure({ adapter: new Adapter() }); + + it('Renders field list component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/dashboards-observability/public/components/explorer/visualizations/__tests__/lens_field_icon.test.tsx b/dashboards-observability/public/components/explorer/visualizations/__tests__/lens_field_icon.test.tsx new file mode 100644 index 000000000..bdc846feb --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/__tests__/lens_field_icon.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { LensFieldIcon } from '../lens_field_icon'; + +describe('Visualization field icon component', () => { + configure({ adapter: new Adapter() }); + + it('Renders field icon component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap new file mode 100644 index 000000000..3b213ff7d --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Assets components Renders lens icon of bar component 1`] = ` + + + + + + +`; + +exports[`Assets components Renders lens icon of horizontal bar component 1`] = ` + + + + + + +`; + +exports[`Assets components Renders lens icon of line component 1`] = ` + + + + + + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/assets.test.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/assets.test.tsx new file mode 100644 index 000000000..3b3263533 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/assets/__tests__/assets.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { LensIconChartBar } from '../chart_bar'; +import { LensIconChartBarHorizontal } from '../chart_bar_horizontal'; +import { LensIconChartLine } from '../chart_line'; + + +describe('Assets components', () => { + configure({ adapter: new Adapter() }); + + it('Renders lens icon of bar component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders lens icon of horizontal bar component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders lens icon of line component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx deleted file mode 100644 index caee9b6fe..000000000 --- a/dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as React from 'react'; - -export const EuiIconLegend = ({ title, titleId, ...props }: { title: string; titleId: string }) => ( - - {title ? {title} : null} - - - - - - - -); diff --git a/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap new file mode 100644 index 000000000..319bdb828 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -0,0 +1,3666 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config panel component Renders config panel wrapper component with fields 1`] = ` + + + , + "id": "setting-panel", + "name": "Settings", + }, + ] + } + > +
+ +
+ + + +
+
+
+ + +
+
+ + +
+ +

+ X-axis +

+
+ +
+ + +
+ + +
+
+
+ + + agent + + + +
+ +
+
+ +
+ +
+ + + + + + + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ + +
+ +

+ Y-axis +

+
+ +
+ + +
+ + +
+
+
+

+ Select a field +

+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+
+
+
+
+
+
+
+`; + +exports[`Config panel component Renders empty config panel wrapper component 1`] = ` + + + , + "id": "setting-panel", + "name": "Settings", + }, + ] + } + > +
+ +
+ + + +
+
+
+ + +
+
+ + +
+ +

+ X-axis +

+
+ +
+ + +
+ + +
+
+
+

+ Select a field +

+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ + +
+ +

+ Y-axis +

+
+ +
+ + +
+ + +
+
+
+

+ Select a field +

+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+
+
+
+
+
+
+
+`; + +exports[`Config panel component Renders panel item component 1`] = ` + + + , + "id": "setting-panel", + "name": "Settings", + }, + ] + } + > +
+ +
+ + + +
+
+
+ + +
+
+ + +
+ +

+ X-axis +

+
+ +
+ + +
+ + +
+
+
+ + + agent + + + +
+ +
+
+ +
+ +
+ + + + + + + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+ + +
+ +

+ Y-axis +

+
+ +
+ + +
+ + +
+
+
+

+ Select a field +

+ +
+ +
+
+ +
+ +
+ + + +
+
+
+
+ + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+ here goes advanced setting +
+
+
+
+
+
+
+
+
+ + + +
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/config_panel.test.tsx b/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/config_panel.test.tsx new file mode 100644 index 000000000..9e46476aa --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/config_panel/__tests__/config_panel.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { ConfigPanelWrapper } from '../config_panel'; +import { + SELECTED_FIELDS, + AVAILABLE_FIELDS, + UNSELECTED_FIELDS, + QUERIED_FIELDS +} from '../../../../../../common/constants/explorer'; +import { + AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, + QUERY_FIELDS +} from '../../../../../../test/event_analytics_constants'; + +describe('Config panel component', () => { + configure({ adapter: new Adapter() }); + + it('Renders empty config panel wrapper component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders config panel wrapper component with fields', async () => { + + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS + }; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders panel item component', async () => { + + const explorerFields = { + [SELECTED_FIELDS]: [], + [UNSELECTED_FIELDS]: [], + [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, + [QUERIED_FIELDS]: QUERY_FIELDS + }; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap new file mode 100644 index 000000000..19230150b --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap @@ -0,0 +1,481 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Count distribution component Renders count distribution component with data 1`] = ` + + + + +
+ + + + +`; + +exports[`Count distribution component Renders count distribution component with data 2`] = ` + + + + +
+ + + + +`; + +exports[`Count distribution component Renders empty count distribution component 1`] = ``; diff --git a/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/count_distribution.test.tsx b/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/count_distribution.test.tsx new file mode 100644 index 000000000..9ae80ea7c --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/count_distribution/__tests__/count_distribution.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { CountDistribution } from '../count_distribution'; +import { SAMPLE_VISUALIZATIONS } from '../../../../../../test/event_analytics_constants'; + +describe('Count distribution component', () => { + configure({ adapter: new Adapter() }); + + it('Renders empty count distribution component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders count distribution component with data', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap new file mode 100644 index 000000000..7fc0731ee --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap @@ -0,0 +1,198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Shared components Renders empty placeholder component 1`] = ` + + +
+ +
+ +
+ + + + + + + + + +
+ +

+ + + No results found + + +

+
+ +
+ +
+ + +`; + +exports[`Shared components Renders tool bar button component 1`] = ` + + + + + + + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/shared_components.test.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/shared_components.test.tsx new file mode 100644 index 000000000..4871839ec --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/shared_components/__tests__/shared_components.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { EmptyPlaceholder } from '../empty_placeholder'; +import { LensIconChartBar } from '../../assets/chart_bar'; +import { ToolbarButton } from '../toolbar_button'; + +describe('Shared components', () => { + configure({ adapter: new Adapter() }); + + it('Renders empty placeholder component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders tool bar button component', async () => { + + const handleClick = jest.fn(); + const WrappedComponent = () =>
testing
; + + const wrapper = mount( + + + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts b/dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts index 9e5a2eace..dfbd82f35 100644 --- a/dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts +++ b/dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts @@ -6,4 +6,3 @@ export * from './empty_placeholder'; export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; export { ToolbarButtonProps, ToolbarButton } from './toolbar_button'; -export { LegendSettingsPopover } from './legend_settings_popover'; diff --git a/dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx deleted file mode 100644 index 1ef54f635..000000000 --- a/dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiButtonGroup, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -import { Position } from '@elastic/charts'; -import { ToolbarPopover } from '../shared_components'; - -export interface LegendSettingsPopoverProps { - /** - * Determines the legend display options - */ - legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide' | 'default'; label: string }>; - /** - * Determines the legend mode - */ - mode: 'default' | 'show' | 'hide' | 'auto'; - /** - * Callback on display option change - */ - onDisplayChange: (id: string) => void; - /** - * Sets the legend position - */ - position?: Position; - /** - * Callback on position option change - */ - onPositionChange: (id: string) => void; - /** - * If true, nested legend switch is rendered - */ - renderNestedLegendSwitch?: boolean; - /** - * nested legend switch status - */ - nestedLegend?: boolean; - /** - * Callback on nested switch status change - */ - onNestedLegendChange?: (event: EuiSwitchEvent) => void; -} - -const toggleButtonsIcons = [ - { - id: Position.Bottom, - label: i18n.translate('xpack.lens.shared.legendPositionBottom', { - defaultMessage: 'Bottom', - }), - iconType: 'arrowDown', - }, - { - id: Position.Left, - label: i18n.translate('xpack.lens.shared.legendPositionLeft', { - defaultMessage: 'Left', - }), - iconType: 'arrowLeft', - }, - { - id: Position.Right, - label: i18n.translate('xpack.lens.shared.legendPositionRight', { - defaultMessage: 'Right', - }), - iconType: 'arrowRight', - }, - { - id: Position.Top, - label: i18n.translate('xpack.lens.shared.legendPositionTop', { - defaultMessage: 'Top', - }), - iconType: 'arrowUp', - }, -]; - -export const LegendSettingsPopover: React.FunctionComponent = ({ - legendOptions, - mode, - onDisplayChange, - position, - onPositionChange, - renderNestedLegendSwitch, - nestedLegend, - onNestedLegendChange = () => {}, -}) => { - return ( - - - value === mode)!.id} - onChange={onDisplayChange} - /> - - - - - {renderNestedLegendSwitch && ( - - - - )} - - ); -}; diff --git a/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx deleted file mode 100644 index 4d2c7527c..000000000 --- a/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { EuiFlexItem, EuiPopover, EuiIcon, EuiPopoverTitle, IconType } from '@elastic/eui'; -import { ToolbarButton, ToolbarButtonProps } from './toolbar_button'; -import { EuiIconLegend } from '../assets/legend'; - -const typeToIconMap: { [type: string]: string | IconType } = { - legend: EuiIconLegend as IconType, - values: 'visText', -}; - -export interface ToolbarPopoverProps { - /** - * Determines popover title - */ - title: string; - /** - * Determines the button icon - */ - type: 'legend' | 'values' | IconType; - /** - * Determines if the popover is disabled - */ - isDisabled?: boolean; - /** - * Button group position - */ - groupPosition?: ToolbarButtonProps['groupPosition']; - buttonDataTestSubj?: string; -} - -export const ToolbarPopover: React.FunctionComponent = ({ - children, - title, - type, - isDisabled = false, - groupPosition, - buttonDataTestSubj, -}) => { - const [open, setOpen] = useState(false); - - const iconType: string | IconType = typeof type === 'string' ? typeToIconMap[type] : type; - - return ( - - { - setOpen(!open); - }} - title={title} - hasArrow={false} - isDisabled={isDisabled} - groupPosition={groupPosition} - dataTestSubj={buttonDataTestSubj} - > - { title } - - - } - isOpen={open} - closePopover={() => { - setOpen(false); - }} - anchorPosition="downRight" - > - {title} - {children} - - - ); -}; diff --git a/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/__snapshots__/workspace.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/__snapshots__/workspace.test.tsx.snap new file mode 100644 index 000000000..c02bfcabc --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/__snapshots__/workspace.test.tsx.snap @@ -0,0 +1,3881 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualization chart switch components Renders workspace with bar component 1`] = ` + +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`Visualization chart switch components Renders workspace with horionzontal bar component 1`] = ` + +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`Visualization chart switch components Renders workspace with line bar component 1`] = ` + +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`Visualization workspace panel Renders workspace panel 1`] = ` + + , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + } + } + visualizationTypes={ + Array [ + Object { + "chart": , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + }, + Object { + "chart": , + "fullLabel": "H. Bar", + "icon": [Function], + "id": "horizontal_bar", + "label": "H. Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-horizontal-bar-2", + }, + Object { + "chart": , + "fullLabel": "Line", + "icon": [Function], + "id": "line", + "label": "Line", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-line-3", + }, + ] + } + > +
+ +
+ +
+ , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + } + } + visualizationTypes={ + Array [ + Object { + "chart": , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + }, + Object { + "chart": , + "fullLabel": "H. Bar", + "icon": [Function], + "id": "horizontal_bar", + "label": "H. Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-horizontal-bar-2", + }, + Object { + "chart": , + "fullLabel": "Line", + "icon": [Function], + "id": "line", + "label": "Line", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-line-3", + }, + ] + } + > +
+ + + } + fullLabel="Bar" + icon={[Function]} + id="bar" + label="Bar" + selection={ + Object { + "dataLoss": "nothing", + } + } + visualizationId="vis-bar-1" + /> + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
+ + +
+ + + +
+ + + +
+ + +
+ +
+ + + + +`; + +exports[`Visualization workspace panel Renders workspace panel 2`] = ` + + , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + } + } + visualizationTypes={ + Array [ + Object { + "chart": , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + }, + Object { + "chart": , + "fullLabel": "H. Bar", + "icon": [Function], + "id": "horizontal_bar", + "label": "H. Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-horizontal-bar-2", + }, + Object { + "chart": , + "fullLabel": "Line", + "icon": [Function], + "id": "line", + "label": "Line", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-line-3", + }, + ] + } + > +
+ +
+ +
+ , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + } + } + visualizationTypes={ + Array [ + Object { + "chart": , + "fullLabel": "Bar", + "icon": [Function], + "id": "bar", + "label": "Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-bar-1", + }, + Object { + "chart": , + "fullLabel": "H. Bar", + "icon": [Function], + "id": "horizontal_bar", + "label": "H. Bar", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-horizontal-bar-2", + }, + Object { + "chart": , + "fullLabel": "Line", + "icon": [Function], + "id": "line", + "label": "Line", + "selection": Object { + "dataLoss": "nothing", + }, + "visualizationId": "vis-line-3", + }, + ] + } + > +
+ + + } + fullLabel="Bar" + icon={[Function]} + id="bar" + label="Bar" + selection={ + Object { + "dataLoss": "nothing", + } + } + visualizationId="vis-bar-1" + /> + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
+ + +
+ + + +
+ + + +
+ + +
+ +
+ + + + +`; + +exports[`Visualization workspace panel wrapper Renders workspace panel wrapper 1`] = ` + +
+ +
+ +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="lnsChartSwitchPopover" + initialFocus=".lnsChartSwitch__popoverPanel" + isOpen={false} + ownFocus={true} + panelClassName="lnsChartSwitch__popoverPanel" + panelPaddingSize="s" + > + +
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
+ +
+ + + +`; diff --git a/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/workspace.test.tsx b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/workspace.test.tsx new file mode 100644 index 000000000..d41e49804 --- /dev/null +++ b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/__tests__/workspace.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import { forEach } from 'lodash'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { ChartSwitch } from '../chart_switch'; +import { Bar } from '../../../../visualizations/charts/bar'; +import { HorizontalBar } from '../../../../visualizations/charts/horizontal_bar' +import { Line } from '../../../../visualizations/charts/line'; +import { LensIconChartBar } from '../../assets/chart_bar'; +import { LensIconChartBarHorizontal } from '../../assets/chart_bar_horizontal'; +import { LensIconChartLine } from '../../assets/chart_line'; +import { + VISUALIZATION_TYPES, + SAMPLE_VISUALIZATIONS +} from '../../../../../../test/event_analytics_constants'; +import { WorkspacePanel } from '../workspace_panel'; +import { WorkspacePanelWrapper } from '../workspace_panel_wrapper'; + +const attachVisualizationComponents = () => { + VISUALIZATION_TYPES[0]['chart'] = () => ; + VISUALIZATION_TYPES[0]['icon'] = () => ; + VISUALIZATION_TYPES[1]['chart'] = () => ; + VISUALIZATION_TYPES[1]['icon'] = () => ; + VISUALIZATION_TYPES[2]['chart'] = () => ; + VISUALIZATION_TYPES[2]['icon'] = () => ; +}; + +describe('Visualization chart switch components', () => { + configure({ adapter: new Adapter() }); + beforeAll(() => { + attachVisualizationComponents(); + }); + + it('Renders workspace with bar component', async () => { + + const setVis = jest.fn(); + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders workspace with horionzontal bar component', async () => { + + const setVis = jest.fn(); + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders workspace with line bar component', async () => { + + const setVis = jest.fn(); + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); + +describe('Visualization workspace panel', () => { + it('Renders workspace panel', async () => { + + const setCurVisId = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); + +describe('Visualization workspace panel wrapper', () => { + it('Renders workspace panel wrapper', async () => { + + const setVis = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx index 01bfc6e1b..6fd0b3ac0 100644 --- a/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx +++ b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx @@ -41,9 +41,6 @@ interface IWorkSpacePanel { curVisId: string; setCurVisId: any; visualizations: any; - savedObjects: SavedObjects; - onSaveVisualization: any; - getSavedObjects: any; } export function WorkspacePanel({ diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap new file mode 100644 index 000000000..9a16cf5d6 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap @@ -0,0 +1,389 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Bar component Renders bar component 1`] = ` + + + +
+ + + +`; + +exports[`Bar component Renders bar component 2`] = ` + + + +
+ + + +`; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap new file mode 100644 index 000000000..f73261fc7 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap @@ -0,0 +1,567 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Horizontal bar component Renders horizontal bar component 1`] = ` + + + + +
+ + + + +`; + +exports[`Horizontal bar component Renders horizontal bar component 2`] = ` + + + + +
+ + + + +`; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap new file mode 100644 index 000000000..915d62476 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap @@ -0,0 +1,397 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Line component Renders line component 1`] = ` + + + +
+ + + +`; + +exports[`Line component Renders line component 2`] = ` + + + +
+ + + +`; diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/bar.test.tsx b/dashboards-observability/public/components/visualizations/charts/__tests__/bar.test.tsx new file mode 100644 index 000000000..d56af8afe --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/bar.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Bar } from '../bar'; +import { + LAYOUT_CONFIG, + SAMPLE_VISUALIZATIONS +} from '../../../../../test/event_analytics_constants'; + +describe('Bar component', () => { + configure({ adapter: new Adapter() }); + + it('Renders bar component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/horizontal_bar.test.tsx b/dashboards-observability/public/components/visualizations/charts/__tests__/horizontal_bar.test.tsx new file mode 100644 index 000000000..a3ae0a5e1 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/horizontal_bar.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { HorizontalBar } from '../horizontal_bar'; +import { + LAYOUT_CONFIG, + SAMPLE_VISUALIZATIONS +} from '../../../../../test/event_analytics_constants'; + +describe('Horizontal bar component', () => { + configure({ adapter: new Adapter() }); + + it('Renders horizontal bar component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/visualizations/charts/__tests__/line.test.tsx b/dashboards-observability/public/components/visualizations/charts/__tests__/line.test.tsx new file mode 100644 index 000000000..8a4c3515e --- /dev/null +++ b/dashboards-observability/public/components/visualizations/charts/__tests__/line.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Line } from '../line'; +import { + LAYOUT_CONFIG, + SAMPLE_VISUALIZATIONS +} from '../../../../../test/event_analytics_constants'; + +describe('Line component', () => { + configure({ adapter: new Adapter() }); + + it('Renders line component', async () => { + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap b/dashboards-observability/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap new file mode 100644 index 000000000..a426dfc52 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ploty base component Renders Ploty base component 1`] = ` + + +
+ + +`; + +exports[`Ploty base component Renders Ploty base component 2`] = ` + + +
+ + +`; diff --git a/dashboards-observability/public/components/visualizations/plotly/__tests__/plotly.test.tsx b/dashboards-observability/public/components/visualizations/plotly/__tests__/plotly.test.tsx new file mode 100644 index 000000000..02fab5123 --- /dev/null +++ b/dashboards-observability/public/components/visualizations/plotly/__tests__/plotly.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Plt } from '../plot'; +import { LAYOUT_CONFIG } from '../../../../../test/event_analytics_constants'; + +describe('Ploty base component', () => { + configure({ adapter: new Adapter() }); + + it('Renders Ploty base component', async () => { + + const barValues = [{ + displaylogo: false, + marker: { + color: [ + "#3CA1C7", + "#8C55A3", + "#DB748A", + "#F2BE4B" + ] + }, + name: "avg(FlightDelayMin)", + responsive: true, + type: 'bar', + x: [ + "JetBeats", + "Logstash Airways", + "OpenSearch Dashboards Airlines", + "OpenSearch-Air" + ], + y: [ + 45.957544288332315, + 49.55268688081657, + 46.368274582560296, + 47.41304347826087 + ] + }]; + + const wrapper = mount( + + ); + + wrapper.update(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/dashboards-observability/test/event_analytics_constants.ts b/dashboards-observability/test/event_analytics_constants.ts new file mode 100644 index 000000000..e35504e05 --- /dev/null +++ b/dashboards-observability/test/event_analytics_constants.ts @@ -0,0 +1,472 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LONG_CHART_COLOR } from '../common/constants/shared'; + +export const AVAILABLE_FIELDS = [ + { + name: 'agent', + type: 'string' + }, + { + name: 'bytes', + type: 'long' + }, + { + name: 'clientip', + type: 'ip' + }, + { + name: 'event', + type: 'struct' + }, + { + name: 'extension', + type: 'string' + }, + { + name: 'geo', + type: 'struct' + }, + { + name: 'host', + type: 'string' + }, + { + name: 'index', + type: 'string' + }, + { + name: 'ip', + type: 'ip' + }, + { + name: 'machine', + type: 'struct' + }, + { + name: 'memory', + type: 'double' + }, + { + name: 'message', + type: 'string' + }, + { + name: 'phpmemory', + type: 'long' + }, + { + name: 'referer', + type: 'string' + }, + { + name: 'request', + type: 'string' + }, + { + name: 'response', + type: 'string' + }, + { + name: 'tags', + type: 'string' + }, + { + name: 'timestamp', + type: 'timestamp' + }, + { + name: 'url', + type: 'string' + }, + { + name: 'utc_time', + type: 'timestamp' + } +]; + +export const QUERY_FIELDS = [ + { + name: 'double_per_ip_bytes', + type: 'long' + }, + { + name: 'host', + type: 'text' + }, + { + name: 'ip_count', + type: 'integer' + }, + { + name: 'per_ip_bytes', + type: 'long' + }, + { + name: 'resp_code', + type: 'text' + }, + { + name: 'sum_bytes', + type: 'long' + } +]; + +export const JSON_DATA = [ + { + ip_count: 176, + sum_bytes: 1021420, + host: 'artifacts.opensearch.org', + resp_code: '404', + per_ip_bytes: 5803, + double_per_ip_bytes: 11606 + }, + { + ip_count: 111, + sum_bytes: 560638, + host: 'www.opensearch.org', + resp_code: '404', + per_ip_bytes: 5050, + double_per_ip_bytes: 10100 + }, + { + ip_count: 94, + sum_bytes: 0, + host: 'artifacts.opensearch.org', + resp_code: '503', + per_ip_bytes: 0, + double_per_ip_bytes: 0 + }, + { + ip_count: 78, + sum_bytes: 0, + host: 'www.opensearch.org', + resp_code: '503', + per_ip_bytes: 0, + double_per_ip_bytes: 0 + }, + { + ip_count: 43, + sum_bytes: 247840, + host: 'cdn.opensearch-opensearch-opensearch.org', + resp_code: '404', + per_ip_bytes: 5763, + double_per_ip_bytes: 11526 + }, + { + ip_count: 34, + sum_bytes: 0, + host: 'cdn.opensearch-opensearch-opensearch.org', + resp_code: '503', + per_ip_bytes: 0, + double_per_ip_bytes: 0 + }, + { + ip_count: 13, + sum_bytes: 57735, + host: 'opensearch-opensearch-opensearch.org', + resp_code: '404', + per_ip_bytes: 4441, + double_per_ip_bytes: 8882 + }, + { + ip_count: 6, + sum_bytes: 0, + host: 'opensearch-opensearch-opensearch.org', + resp_code: '503', + per_ip_bytes: 0, + double_per_ip_bytes: 0 + } +]; + +export const JSON_DATA_ALL = [ + { + referer: 'http://twitter.com/success/wendy-lawrence', + request: '/opensearch/opensearch-1.0.0.deb', + agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1', + extension: 'deb', + memory: 'null', + geo: '{"srcdest":"IN:US","src":"IN","coordinates":{"lat":39.41042861,"lon":-88.8454325},"dest":"US"}', + utc_time: '2021-11-14 00:39:02.912', + clientip: '223.87.60.27', + host: 'artifacts.opensearch.org', + event: '{"dataset":"sample_web_logs"}', + phpmemory: 'null', + timestamp: '2021-11-14 00:39:02.912', + ip: '223.87.60.27', + index: 'opensearch_dashboards_sample_data_logs', + message: '223.87.60.27 - - [2018-07-22T00:39:02.912Z] "GET /opensearch/opensearch-1.0.0.deb_1 HTTP/1.1" 200 6219 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"', + url: 'https://artifacts.opensearch.org/downloads/opensearch/opensearch-1.0.0.deb_1', + tags: 'success', + bytes: 6219, + machine: '{"os":"win 8","ram":8589934592}', + response: '200' + }, + { + referer: 'http://www.opensearch-opensearch-opensearch.com/success/james-mcdivitt', + request: '/beats/metricbeat', + agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1', + extension: '', + memory: 'null', + geo: '{"srcdest":"JP:IN","src":"JP","coordinates":{"lat":38.58338806,"lon":-86.46248778},"dest":"IN"}', + utc_time: '2021-11-14 03:26:21.326', + clientip: '130.246.123.197', + host: 'www.opensearch.org', + event: '{"dataset":"sample_web_logs"}', + phpmemory: 'null', + timestamp: '2021-11-14 03:26:21.326', + ip: '130.246.123.197', + index: 'opensearch_dashboards_sample_data_logs', + message: '130.246.123.197 - - [2018-07-22T03:26:21.326Z] "GET /beats/metricbeat_1 HTTP/1.1" 200 6850 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"', + url: 'https://www.opensearch.org/downloads/beats/metricbeat_1', + tags: 'success', + bytes: 6850, + machine: '{"os":"win 8","ram":3221225472}', + response: '200', + }, + { + referer: 'http://twitter.com/success/konstantin-feoktistov', + request: '/styles/main.css', + agent: 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24', + extension: 'css', + memory: 'null', + geo: '{"srcdest":"CO:DE","src":"CO","coordinates":{"lat":36.96015,"lon":-78.18499861},"dest":"DE"}', + utc_time: '2021-11-14 03:30:25.131', + clientip: '120.49.143.213', + host: 'cdn.opensearch-opensearch-opensearch.org', + event: '{"dataset":"sample_web_logs"}', + phpmemory: 'null', + timestamp: '2021-11-14 03:30:25.131', + ip: '120.49.143.213', + index: 'opensearch_dashboards_sample_data_logs', + message: '120.49.143.213 - - [2018-07-22T03:30:25.131Z] "GET /styles/main.css_1 HTTP/1.1" 503 0 "-" "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24"', + url: 'https://cdn.opensearch-opensearch-opensearch.org/styles/main.css_1', + tags: 'success', + bytes: 0, + machine: '{"os":"ios","ram":20401094656}', + response: '503' + }, +]; + +export const AGENT_FIELD = { + name: 'agent', + type: 'string' +}; + +export const SAVED_HISTORIES = [ + { + createdTimeMs: '1638901792922', + lastUpdatedTimeMs: '1638901792922', + objectId: 'Kocoln0BYMuJGDsOwDma', + savedVisualization: { + description: '', + name: 'Mock Flight count by destination save to panel', + query: 'source = opensearch_dashboards_sample_data_flights | stats avg(FlightDelayMin) by Carrier', + type: 'bar', + selected_date_range: { + end: 'now', + start: 'now-15m', + text: '' + }, + selected_fields: { + text: '', + tokens: [] + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp' + } + }, + tenant: '', + }, + { + createdTimeMs: '1638901777572', + lastUpdatedTimeMs: '1638901777572', + objectId: 'KIcoln0BYMuJGDsOhDmk', + savedVisualization: { + description: '', + name: 'Mock Flight count by destination', + query: 'source = opensearch_dashboards_sample_data_flights | stats avg(FlightDelayMin) by Carrier', + type: 'bar', + selected_date_range: { + end: 'now', + start: 'now-15m', + text: '' + }, + selected_fields: { + text: '', + tokens: [] + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp' + } + }, + tenant: '', + } +]; + +export const SELECTED_PANELS_OPTIONS = [ + { + label: '[Logs] Web traffic Panel', + panel: { + dateCreated: 1637781403888, + dateModified: 1637781403888, + id: 'uRZgU30B661cwDZT-ILw', + name: '[Logs] Web traffic Panel' + }, + }, + { + label: '[Logs] Web traffic Panel 2', + panel: { + dateCreated: 1637781403888, + dateModified: 1637781403888, + id: 'uRZgU30B661cwDZT-ILw', + name: '[Logs] Web traffic Panel' + }, + } +]; + +export const DATA_GRID_ROWS = [ + { + AvgTicketPrice: 841.2656, + Cancelled: 'false', + Carrier: 'OpenSearch Dashboards Airlines', + Dest: 'Sydney Kingsford Smith International Airport', + DestAirportID: 'SYD', + DestCityName: 'Sydney', + DestCountry: 'AU', + DestLocation: '{\"lat\":-33.94609833,\"lon\":151.177002}', + DestRegion: 'SE-BD', + DestWeather: "Rain", + DistanceKilometers: 16492.326, + DistanceMiles: 10247.856, + FlightDelay: 'false', + FlightDelayMin: 0, + FlightDelayType: 'No Delay', + FlightNum: '9HY9SWR', + FlightTimeHour: '17.179506930998397', + FlightTimeMin: 1030.7704, + Origin: 'Frankfurt am Main Airport', + OriginAirportID: "FRA", + OriginCityName: 'Frankfurt am Main', + OriginCountry: 'DE', + OriginLocation: '{\"lat\":50.033333,\"lon\":8.570556}', + OriginRegion: 'DE-HE', + OriginWeather: 'Sunny', + dayOfWeek: 0, + timestamp: "2021-05-24 00:00:00" + }, + { + AvgTicketPrice: 882.98267, + Cancelled: 'false', + Carrier: 'Logstash Airways', + Dest: 'Venice Marco Polo Airport', + DestAirportID: 'VE05', + DestCityName: 'Venice', + DestCountry: 'IT', + DestLocation: '{\"lat\":45.505299,\"lon\":12.3519}', + DestRegion: 'IT-34', + DestWeather: "Sunny", + DistanceKilometers: 8823.4, + DistanceMiles: 5482.6064, + FlightDelay: 'false', + FlightDelayMin: 0, + FlightDelayType: 'No Delay', + FlightNum: 'X98CCZO', + FlightTimeHour: '7.73982468459836', + FlightTimeMin: 464.3895, + Origin: 'Cape Town International Airport', + OriginAirportID: "CPT", + OriginCityName: 'Cape Town', + OriginCountry: 'ZA', + OriginLocation: '{\"lat\":-33.96480179,\"lon\":18.60169983}', + OriginRegion: 'SE-BD', + OriginWeather: 'Clear', + dayOfWeek: 0, + timestamp: "2021-05-24 18:27:00" + } +]; + +export const SAMPLE_VISUALIZATIONS = { + data: { + 'count()': [2549, 9337, 1173], + 'span(timestamp,1M)': [ + '2021-05-01 00:00:00', + '2021-06-01 00:00:00', + '2021-07-01 00:00:00' + ], + }, + jsonData: [ + { + 'count()': 2549, + 'span(timestamp,1M)': '2021-05-01 00:00:00' + }, + { + 'count()': 9337, + 'span(timestamp,1M)': '2021-06-01 00:00:00' + }, + { + 'count()': 2549, + 'span(timestamp,1M)': '2021-07-01 00:00:00' + } + ], + metadata: { + fields: [ + { + name: 'count()', + type: 'integer' + }, + { + name: 'span(timestamp,1M)', + type: 'timestamp' + } + ] + } +}; + +export const VISUALIZATION_TYPES = [ + { + fullLabel: 'Bar', + id: 'bar', + label: 'bar', + selection: { + dataLoss: 'nothing' + }, + visualizationId: 'vis-bar-6636' + }, + { + fullLabel: 'H. Bar', + id: 'horizontal_bar', + label: 'H. Bar', + selection: { + dataLoss: 'nothing' + }, + visualizationId: 'vis-bar-6637' + }, + { + fullLabel: 'Line', + id: 'line', + label: 'line', + selection: { + dataLoss: 'nothing' + }, + visualizationId: 'vis-bar-6638' + } +]; + +export const LAYOUT_CONFIG = { + showlegend: true, + margin: { + l: 60, + r: 10, + b: 15, + t: 30, + pad: 0, + }, + height: 220, + colorway: [LONG_CHART_COLOR], +}; \ No newline at end of file From d1cbfa0b5086de3025acb4f132797826dea0d177 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 14 Dec 2021 15:34:14 -0800 Subject: [PATCH 04/16] Update service map parsing results for testing (#345) Signed-off-by: Joshua Li --- dashboards-observability/test/constants.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dashboards-observability/test/constants.ts b/dashboards-observability/test/constants.ts index 1a7d7c65c..04bb4f23b 100644 --- a/dashboards-observability/test/constants.ts +++ b/dashboards-observability/test/constants.ts @@ -148,7 +148,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 1, label: 'order', size: 15, - title: '

order

Average latency: 90.1ms

', + title: 'order\n\nAverage latency: 90.1ms', borderWidth: 0, color: 'rgba(158, 134, 192, 1)', }, @@ -156,7 +156,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 2, label: 'analytics-service', size: 15, - title: '

analytics-service

Average latency: 12.99ms

', + title: 'analytics-service\n\nAverage latency: 12.99ms', borderWidth: 0, color: 'rgba(210, 202, 224, 1)', }, @@ -164,7 +164,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 3, label: 'database', size: 15, - title: '

database

Average latency: 49.54ms

', + title: 'database\n\nAverage latency: 49.54ms', borderWidth: 0, color: 'rgba(187, 171, 212, 1)', }, @@ -172,7 +172,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 4, label: 'frontend-client', size: 15, - title: '

frontend-client

Average latency: 207.71ms

', + title: 'frontend-client\n\nAverage latency: 207.71ms', borderWidth: 0, color: 'rgba(78, 42, 122, 1)', }, @@ -180,7 +180,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 5, label: 'inventory', size: 15, - title: '

inventory

Average latency: 183.52ms

', + title: 'inventory\n\nAverage latency: 183.52ms', borderWidth: 0, color: 'rgba(95, 61, 138, 1)', }, @@ -188,7 +188,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 6, label: 'authentication', size: 15, - title: '

authentication

Average latency: 139.09ms

', + title: 'authentication\n\nAverage latency: 139.09ms', borderWidth: 0, color: 'rgba(125, 95, 166, 1)', }, @@ -196,7 +196,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 7, label: 'payment', size: 15, - title: '

payment

Average latency: 134.36ms

', + title: 'payment\n\nAverage latency: 134.36ms', borderWidth: 0, color: 'rgba(129, 99, 169, 1)', }, @@ -204,7 +204,7 @@ export const TEST_SERVICE_MAP_GRAPH = { id: 8, label: 'recommendation', size: 15, - title: '

recommendation

Average latency: 176.97ms

', + title: 'recommendation\n\nAverage latency: 176.97ms', borderWidth: 0, color: 'rgba(100, 66, 143, 1)', }, From 249c6b615f28255771d7b65b0c895c751618c37f Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Wed, 15 Dec 2021 08:34:19 -0800 Subject: [PATCH 05/16] bumping version to 1.2.2 (#346) * bumping version to 1.2.2 Signed-off-by: Shenoy Pratik * update PR in release notes Signed-off-by: Shenoy Pratik --- .../opensearch-observability-test-and-build-workflow.yml | 2 +- opensearch-observability/build.gradle | 4 ++-- .../opensearch-trace-analytics.release-notes-1.2.2.0.md | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 release-notes/opensearch-trace-analytics.release-notes-1.2.2.0.md diff --git a/.github/workflows/opensearch-observability-test-and-build-workflow.yml b/.github/workflows/opensearch-observability-test-and-build-workflow.yml index 307df7332..46630accf 100644 --- a/.github/workflows/opensearch-observability-test-and-build-workflow.yml +++ b/.github/workflows/opensearch-observability-test-and-build-workflow.yml @@ -3,7 +3,7 @@ name: Test and Build OpenSearch Observability Backend Plugin on: [pull_request, push] env: - OPENSEARCH_VERSION: '1.2.1-SNAPSHOT' + OPENSEARCH_VERSION: '1.2.2-SNAPSHOT' OPENSEARCH_BRANCH: '1.2' COMMON_UTILS_BRANCH: 'main' diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 906a0aec8..9a6c29c81 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,7 +9,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.2.1-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "1.2.2-SNAPSHOT") // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -262,7 +262,7 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" - versions = ["1.1.0","1.2.1-SNAPSHOT"] + versions = ["1.1.0","1.2.2-SNAPSHOT"] numberOfNodes = 3 plugin(provider(new Callable(){ @Override diff --git a/release-notes/opensearch-trace-analytics.release-notes-1.2.2.0.md b/release-notes/opensearch-trace-analytics.release-notes-1.2.2.0.md new file mode 100644 index 000000000..181b01611 --- /dev/null +++ b/release-notes/opensearch-trace-analytics.release-notes-1.2.2.0.md @@ -0,0 +1,6 @@ +## Version 1.2.2.0 Release Notes + +Compatible with OpenSearch Version 1.2.2 and OpenSearch Dashboards Version 1.2.0 + +### Maintenance +* Bump observability version for OpenSearch 1.2.2 release [#346](https://github.com/opensearch-project/observability/pull/346) \ No newline at end of file From fff303aacad5c206b53d7eb4364ff0d2c20df158 Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Mon, 20 Dec 2021 15:37:07 -0800 Subject: [PATCH 06/16] updating readme and badges (#352) Signed-off-by: Shenoy Pratik --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e2d6ef5f..e9d8b671f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ + + +- [Observability](#observability) +- [Code Summary](#code-summary) +- [Plugin Components](#plugin-components) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Getting Help](#getting-help) +- [Code of Conduct](#code-of-conduct) +- [Security](#security) +- [License](#license) +- [Copyright](#copyright) + # Observability -The Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. +Observability is collection of plugins and applications that let you visualize data-driven events by using Piped Processing Language to explore, discover, and query data stored in OpenSearch. ## Code Summary @@ -36,6 +49,8 @@ The Observability plugin has four components: Trace Analytics, Event Analytics, | [![features open][feature-badge]][feature-link] | | [![enhancements open][enhancement-badge]][enhancement-link] | | [![bugs open][bug-badge]][bug-link] | +| [![untriaged open][untriaged-badge]][untriaged-link] | +| [![nolabel open][nolabel-badge]][nolabel-link] | [dco-badge]: https://github.com/opensearch-project/observability/actions/workflows/dco.yml/badge.svg [dco-badge-link]: https://github.com/opensearch-project/observability/actions/workflows/dco.yml @@ -66,6 +81,14 @@ The Observability plugin has four components: Trace Analytics, Event Analytics, [bug-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Abug+ [enhancement-badge]: https://img.shields.io/github/issues/opensearch-project/observability/enhancement.svg [enhancement-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+ +[untriaged-badge]: https://img.shields.io/github/issues/opensearch-project/observability/untriaged.svg +[untriaged-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+label%3Auntriaged+ +[nolabel-badge]: https://img.shields.io/github/issues-search/opensearch-project/observability?color=yellow&label=no%20label%20issues&query=is%3Aopen%20is%3Aissue%20no%3Alabel +[nolabel-link]: https://github.com/opensearch-project/observability/issues?q=is%3Aopen+is%3Aissue+no%3Alabel+ + +## Plugin Components + +The Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. ### Trace Analytics From 2dd78fff98abf06bf844b86f25d587651944646a Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 14 Dec 2021 11:56:03 -0800 Subject: [PATCH 07/16] Application Analytics (#299) * Add database schema for application Signed-off-by: Eugene Lee * Finished front end for Application overview Signed-off-by: Eugene Lee * Finished application detail page tabs Signed-off-by: Eugene Lee * WIP: Overview Page Signed-off-by: Eugene Lee * Rough sketch of App Analytics UI Signed-off-by: Eugene Lee * Create dummy page Signed-off-by: Eugene Lee * Create app complete. Stabilizing dashboard component. Signed-off-by: Eugene Lee * Update to 1.2 Observability Signed-off-by: Eugene Lee * notebooks internal error Signed-off-by: Eugene Lee * Address comments on PR: copyright headers, indentation, unnecessary render props Signed-off-by: Eugene Lee * Set max width of app and event Signed-off-by: Eugene Lee * Remove optional after description Signed-off-by: Eugene Lee * Change to singular Signed-off-by: Eugene Lee * Remove count badge for log source Signed-off-by: Eugene Lee * #290: Change form row label to ppl base query Signed-off-by: Eugene Lee * #291: Change description and help text for log source Signed-off-by: Eugene Lee * Pass down proper props Signed-off-by: Eugene Lee * Resolve gradle error and module not found error Signed-off-by: Eugene Lee * Resolve kotlin errors Signed-off-by: Eugene Lee * Fix parsers Signed-off-by: Eugene Lee * Add praseItemList Signed-off-by: Eugene Lee * Camelcase fields Signed-off-by: Eugene Lee * Remove whitespace, add copyright Signed-off-by: Eugene Lee * #292: Add autocomplete to Log Source accordion Signed-off-by: Eugene Lee * Lexicographic kotlin import Signed-off-by: Eugene Lee * Add newline at end of files Signed-off-by: Eugene Lee * #293: Add service map to create page Signed-off-by: Eugene Lee * #304: Activate Clear All button for services Signed-off-by: Eugene Lee * #305: Add button to clear base query Signed-off-by: Eugene Lee * opensearch-project#295: Add eui combo box for trace groups Signed-off-by: Eugene Lee * Separate out configuration renders Signed-off-by: Eugene Lee * debug adding filters traces Signed-off-by: Eugene Lee * #296: Add traces table to config Signed-off-by: Eugene Lee * Change from tsx to ts Signed-off-by: Eugene Lee * opensearch-project#309: Add page props and add app specific filters Signed-off-by: Eugene Lee * #308: Add button to clear trace groups Signed-off-by: Eugene Lee * #311: Allow services and traces to be selected Signed-off-by: Eugene Lee * Remove link to traces on table Signed-off-by: Eugene Lee * Disable clear all if nothing selected Signed-off-by: Eugene Lee * disable clear all when no log source Signed-off-by: Eugene Lee * Remove comment, add style to constant, temporarily remove availability Signed-off-by: Eugene Lee * Address PR comments Signed-off-by: Eugene Lee * Revert type assignment Signed-off-by: Eugene Lee * Update tests, builds and doc (#318) * rebased with bwc tests Signed-off-by: Shenoy Pratik * updated bwc tests Signed-off-by: Shenoy Pratik * added release notes Signed-off-by: Shenoy Pratik * Fix errors and address comments Signed-off-by: Eugene Lee * #319: Disable create until required fields are filled out Signed-off-by: Eugene Lee * #329: Add missing field tool tip Signed-off-by: Eugene Lee * Remove unnecessary imports Signed-off-by: Eugene Lee * #320: Add clear modal for friction Signed-off-by: Eugene Lee Co-authored-by: Shenoy Pratik --- .../common/constants/application_analytics.ts | 19 ++ .../common/constants/shared.ts | 8 + .../common/types/explorer.ts | 6 +- .../public/components/app.tsx | 21 +- .../components/app_table.tsx | 221 ++++++++++++++ .../components/application.tsx | 244 ++++++++++++++++ .../config_components/log_config.tsx | 108 +++++++ .../config_components/service_config.tsx | 116 ++++++++ .../config_components/trace_config.tsx | 203 +++++++++++++ .../components/configuration.tsx | 152 ++++++++++ .../components/create.tsx | 171 +++++++++++ .../components/helpers/modal_containers.tsx | 40 +++ .../components/application_analytics/home.tsx | 148 ++++++++++ .../common/search/autocomplete.test.tsx | 2 +- .../components/common/search/autocomplete.tsx | 2 +- .../common/search/autocomplete_logic.ts | 267 +++++++++++++++++ .../common/search/autocomplete_logic.tsx | 268 ----------------- .../components/common/search/search.test.tsx | 2 +- .../public/components/common/side_nav.tsx | 7 +- .../custom_panels/custom_panel_table.tsx | 9 +- .../components/explorer/event_analytics.tsx | 11 +- .../public/components/explorer/explorer.tsx | 3 +- .../public/components/explorer/home.tsx | 20 +- .../explorer/home_table/saved_query_table.tsx | 9 +- .../components/explorer/log_explorer.tsx | 18 +- .../notebooks/components/note_table.tsx | 8 +- .../common/filters/filter_helpers.tsx | 11 +- .../components/common/filters/filters.tsx | 2 +- .../components/common/search_bar.tsx | 2 +- .../__tests__/dashboard_table.test.tsx | 2 + .../components/dashboard/dashboard.tsx | 46 ++- .../components/dashboard/dashboard_table.tsx | 7 +- .../components/services/services.tsx | 35 ++- .../traces/__tests__/traces.test.tsx | 5 +- .../components/traces/traces.tsx | 35 ++- .../components/trace_analytics/home.tsx | 6 +- .../application_adaptor.ts | 0 opensearch-observability/build.gradle | 4 + .../observability/model/Application.kt | 270 ++++++++++++++++++ .../ObservabilityObjectDataProperties.kt | 5 + .../model/ObservabilityObjectDoc.kt | 5 + .../model/ObservabilityObjectType.kt | 6 + .../opensearch/observability/model/RestTag.kt | 1 + .../main/resources/observability-mapping.yml | 8 + 44 files changed, 2178 insertions(+), 355 deletions(-) create mode 100644 dashboards-observability/common/constants/application_analytics.ts create mode 100644 dashboards-observability/public/components/application_analytics/components/app_table.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/application.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/configuration.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/create.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx create mode 100644 dashboards-observability/public/components/application_analytics/home.tsx create mode 100644 dashboards-observability/public/components/common/search/autocomplete_logic.ts delete mode 100644 dashboards-observability/public/components/common/search/autocomplete_logic.tsx create mode 100644 dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts create mode 100644 opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt diff --git a/dashboards-observability/common/constants/application_analytics.ts b/dashboards-observability/common/constants/application_analytics.ts new file mode 100644 index 000000000..59ee7704e --- /dev/null +++ b/dashboards-observability/common/constants/application_analytics.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const TAB_OVERVIEW_ID_TXT_PFX = 'app-analytics-overview-'; +export const TAB_SERVICE_ID_TXT_PFX = 'app-analytics-service-'; +export const TAB_TRACE_ID_TXT_PFX = 'app-analytics-trace-'; +export const TAB_LOG_ID_TXT_PFX = 'app-analytics-log-'; +export const TAB_CONFIG_ID_TXT_PFX = 'app-analytics-config-'; +export const TAB_OVERVIEW_TITLE = 'Overview'; +export const TAB_SERVICE_TITLE = 'Services'; +export const TAB_TRACE_TITLE = 'Traces & Spans'; +export const TAB_LOG_TITLE = 'Log Events'; +export const TAB_CONFIG_TITLE = 'Configuration'; + +export interface optionType { + label: string; +} diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 4838a54ab..26cf80bbf 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import CSS from 'csstype'; + // Client route export const PPL_BASE = '/api/ppl'; export const PPL_SEARCH = '/search'; @@ -58,3 +60,9 @@ export const PLOTLY_COLOR = [ ]; export const LONG_CHART_COLOR = PLOTLY_COLOR[1]; + +export const pageStyles: CSS.Properties = { + float: 'left', + width: '100%', + maxWidth: '1130px', +}; diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index af86b24e8..da4018488 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -14,7 +14,7 @@ import { SELECTED_TIMESTAMP, SELECTED_DATE_RANGE } from '../constants/explorer'; - import { HttpStart, NotificationsStart } from '../../../../src/core/public'; + import { CoreStart, HttpStart, NotificationsStart } from '../../../../src/core/public'; import SavedObjects from '../../public/services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from '../../public/services/timestamp/timestamp'; import PPLService from '../../public/services/requests/ppl'; @@ -98,4 +98,6 @@ export interface IExplorerProps { text?: React.ReactChild | undefined, side?: string | undefined ) => void; -} \ No newline at end of file + http: CoreStart['http']; + tabCreatedTypes?: any; +} diff --git a/dashboards-observability/public/components/app.tsx b/dashboards-observability/public/components/app.tsx index 4c7e57642..38560e08a 100644 --- a/dashboards-observability/public/components/app.tsx +++ b/dashboards-observability/public/components/app.tsx @@ -11,6 +11,7 @@ import { CoreStart } from '../../../../src/core/public'; import { observabilityID, observabilityTitle } from '../../common/constants/shared'; import store from '../framework/redux/store'; import { AppPluginStartDependencies } from '../types'; +import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; import { Home as CustomPanelsHome } from './custom_panels/home'; import { EventAnalytics } from './explorer/event_analytics'; import { Main as NotebooksHome } from './notebooks/components/main'; @@ -50,6 +51,24 @@ export const App = ({ <> + { + return ( + + ) + }} + /> ( @@ -108,7 +127,7 @@ export const App = ({ /> ); }} - /> + /> diff --git a/dashboards-observability/public/components/application_analytics/components/app_table.tsx b/dashboards-observability/public/components/application_analytics/components/app_table.tsx new file mode 100644 index 000000000..254f183ac --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/app_table.tsx @@ -0,0 +1,221 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { AppAnalyticsComponentDeps, ApplicationType } from '../home'; +import { pageStyles } from '../../../../common/constants/shared'; + +interface AppTableProps extends AppAnalyticsComponentDeps { + loading: boolean; + applications: Array; + }; + +export function AppTable(props: AppTableProps) { + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const { applications, parentBreadcrumb } = props; + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + } + ]); + }) + + const popoverButton = ( + setIsActionsPopoverOpen(!isActionsPopoverOpen)} + > + Actions + + ); + + const popoverItems: ReactElement[] = [ + + Rename + , + + Duplicate + , + + Delete + , + + Add sample application + , + ]; + + const tableColumns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + render: (value, record) => ( + {_.truncate(value, { length: 100 })} + ), + }, + { + field: 'composition', + name: 'Composition', + sortable: true, + truncateText: true, + }, + { + field: 'currentAvailability', + name: 'Current Availability', + sortable: true, + truncateText: true, + }, + { + field: 'availabilityMetrics', + name: 'Availability Metrics', + sortable: true, + truncateText: true, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + }> + >; + + return ( +
+ + + + + +

Overview

+
+
+
+ + + + +

+ Applications ({applications.length}) +

+
+
+ + + + setIsActionsPopoverOpen(false)} + > + + + + + + Create application + + + + +
+ + {applications.length > 0 ? ( + + ) : ( + <> + + +

No applications

+
+ + + + + Create application + + + + + Add sample applications + + + + + + )} +
+
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/application.tsx b/dashboards-observability/public/components/application_analytics/components/application.tsx new file mode 100644 index 000000000..3e1816570 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/application.tsx @@ -0,0 +1,244 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, + EuiText, + EuiTitle, + } from '@elastic/eui'; +import { LogExplorer } from '../../explorer/log_explorer'; +import { Dashboard } from '../../trace_analytics/components/dashboard'; +import { Services } from '../../trace_analytics/components/services'; +import { Traces } from '../../trace_analytics/components/traces'; +import { SpanDetailPanel } from '../../trace_analytics/components/traces/span_detail_panel'; +import { Configuration } from './configuration'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import React, { ReactChild, useMemo, useState } from 'react'; +import { isEmpty, uniqueId } from 'lodash'; +import { + TAB_CONFIG_ID_TXT_PFX, + TAB_CONFIG_TITLE, + TAB_LOG_ID_TXT_PFX, + TAB_LOG_TITLE, + TAB_OVERVIEW_ID_TXT_PFX, + TAB_OVERVIEW_TITLE, + TAB_SERVICE_ID_TXT_PFX, + TAB_SERVICE_TITLE, + TAB_TRACE_ID_TXT_PFX, + TAB_TRACE_TITLE +} from '../../../../common/constants/application_analytics'; +import { EmptyTabParams, IQueryTab } from '../../../../common/types/explorer'; +import { useHistory } from 'react-router-dom'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { RAW_QUERY } from '../../../../common/constants/explorer'; +import { NotificationsStart } from '../../../../../../src/core/public'; +import { AppAnalyticsComponentDeps } from '../home'; + + +const TAB_OVERVIEW_ID = uniqueId(TAB_OVERVIEW_ID_TXT_PFX); +const TAB_SERVICE_ID = uniqueId(TAB_SERVICE_ID_TXT_PFX); +const TAB_TRACE_ID = uniqueId(TAB_TRACE_ID_TXT_PFX); +const TAB_LOG_ID = uniqueId(TAB_LOG_ID_TXT_PFX); +const TAB_CONFIG_ID = uniqueId(TAB_CONFIG_ID_TXT_PFX); + +export interface DetailTab { + id: string; + label: string; + description: string; + onClick: () => void; + testId: string; +} + +interface AppDetailProps extends AppAnalyticsComponentDeps { + disabled?: boolean; + appId: string; + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + + +export function Application(props: AppDetailProps) { + const { pplService, dslService, timestampUtils, savedObjects, http, notifications } = props; + const [selectedTabId, setSelectedTab] = useState(TAB_OVERVIEW_ID); + const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedTab(selectedTab.id); + const history = useHistory(); + const [toasts, setToasts] = useState>([]); + + const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { + if (!text) text = ''; + setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); + }; + + const getExistingEmptyTab = ({tabIds, queries, explorerData}: EmptyTabParams) => { + let emptyTabId = ''; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; + if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { + emptyTabId = tid; + break; + } + } + return emptyTabId; + }; + + + const getOverview = () => { + return ( + + ); + }; + + const getService = () => { + return ( + + ); + }; + + const getTrace = () => { + return ( + <> + + + + + ); + }; + + const getLog = () => { + return ( + + ); + }; + + const getConfig = () => { + return ( + + ); + }; + + function getAppAnalyticsTab ({ + tabId, + tabTitle, + getContent + }: { + tabId: string, + tabTitle: string, + getContent: () => JSX.Element + }) { + return { + id: tabId, + name: (<> + + { tabTitle } + + ), + content: ( + <> + { getContent() } + ) + }; + }; + + const getAppAnalyticsTabs = () => { + return [ + getAppAnalyticsTab( + { + tabId: TAB_OVERVIEW_ID, + tabTitle: TAB_OVERVIEW_TITLE, + getContent: () => getOverview() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_SERVICE_ID, + tabTitle: TAB_SERVICE_TITLE, + getContent: () => getService() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_TRACE_ID, + tabTitle: TAB_TRACE_TITLE, + getContent: () => getTrace() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_LOG_ID, + tabTitle: TAB_LOG_TITLE, + getContent: () => getLog() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_CONFIG_ID, + tabTitle: TAB_CONFIG_TITLE, + getContent: () => getConfig() + } + ) + ]; + }; + + + const memorizedAppAnalyticsTabs = useMemo(() => { + return getAppAnalyticsTabs(); + }, + []); + + return ( +
+ + + + + +

my-app1

+
+
+
+ { tab.id === selectedTabId }) } + onTabClick={ (selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab) } + tabs={ memorizedAppAnalyticsTabs } + /> +
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx new file mode 100644 index 000000000..297e409c4 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiText, EuiSpacer, EuiButton, EuiFormRow, EuiFlexItem, EuiBadge, EuiOverlayMask } from "@elastic/eui"; +import { uiSettingsService } from "../../../../../common/utils"; +import { Autocomplete } from "../../../common/search/autocomplete"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import{ getClearModal } from "../helpers/modal_containers"; + +interface LogConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + setIsFlyoutVisible: any; +} + +export const LogConfig = (props: LogConfigProps) => { + const { dslService, query, setQuery, setIsFlyoutVisible } = props; + const [logOpen, setLogOpen] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); + const tempQuery =''; + + const handleQueryChange = async (query: string) => setQuery(query); + + const showFlyout = () => { + setIsFlyoutVisible(true); + }; + + const onCancel = () => { + setIsModalVisible(false); + } + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onConfirm = () => { + handleQueryChange(''); + closeModal(); + } + + const clearAllModal = () => { + setModalLayout( + getClearModal( + onCancel, + onConfirm, + 'Clear log source', + 'Are you sure you would like to clear the log source?', + 'Clear' + ) + ); + showModal(); + }; + + return ( +
+ + +

Log Source

+
+ + + Configure your application base query + + + } + extraAction={Clear} + onToggle={(isOpen) => {setLogOpen(isOpen)}} + paddingSize="l" + > + + + {}} + dslService={dslService} + /> + showFlyout()} + onClickAriaLabel={"pplLinkShowFlyout"} + > + PPL + + + +
+ {isModalVisible && modalLayout} +
+ ); +} \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx new file mode 100644 index 000000000..9afed9b4c --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { FilterType } from "../../../trace_analytics/components/common/filters/filters"; +import { ServiceObject } from "../../../trace_analytics/components/common/plots/service_map"; +import { ServiceMap } from "../../../trace_analytics/components/services"; +import { handleServiceMapRequest } from "../../../trace_analytics/requests/services_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { useEffect } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { optionType } from "common/constants/application_analytics"; + +interface ServiceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedServices: Array; + setSelectedServices: (services: Array) => void; +} + +export const ServiceConfig = (props: ServiceConfigProps) => { + const { dslService, filters, setFilters, http, selectedServices, setSelectedServices } = props; + const [servicesOpen, setServicesOpen] = useState(false); + const [serviceMap, setServiceMap] = useState({}); + const [serviceMapIdSelected, setServiceMapIdSelected] = useState<'latency' | 'error_rate' | 'throughput'>('latency'); + + useEffect(() => { + handleServiceMapRequest(http, dslService, serviceMap, setServiceMap); + }, []) + + useEffect (() => { + const serviceOptions = filters.filter(f => f.field === 'serviceName').map((f) => { return { label: f.value }}); + const noDups = serviceOptions.filter((s, index) => { return serviceOptions.findIndex(ser => ser.label === s.label) === index }); + setSelectedServices(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onServiceChange = (selectedServices: any) => { + const serviceFilters = selectedServices.map((option: optionType) => { + return { + field: 'serviceName', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonServiceFilters = filters.filter((f) => f.field !== 'serviceName'); + setFilters([...nonServiceFilters, ...serviceFilters]); + }; + + const clearServices = () => { + const withoutServices = filters.filter((f) => f.field !== 'serviceName') + setFilters(withoutServices); + }; + + const services = Object.keys(serviceMap).map((service) => { return { label: service } }); + + return ( + + +

+ Services & Entities {selectedServices.length} +

+
+ + + Select services & entities to include in this application + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setServicesOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx new file mode 100644 index 000000000..e17fa78fd --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx @@ -0,0 +1,203 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dateMath from '@elastic/datemath'; +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { optionType } from "common/constants/application_analytics"; +import { filtersToDsl } from "../../../trace_analytics/components/common/helper_functions"; +import { handleDashboardRequest } from "../../../trace_analytics/requests/dashboard_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { DashboardTable } from '../../../trace_analytics/components/dashboard/dashboard_table'; +import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; + +interface TraceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedTraces: Array; + setSelectedTraces: (traces: Array) => void; +} + +export const TraceConfig = (props: TraceConfigProps) => { + const { dslService, query, filters, setFilters, http, startTime, endTime, selectedTraces, setSelectedTraces } = props; + const [traceOpen, setTraceOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [traceItems, setTraceItems] = useState([]); + const [traceOptions, setTraceOptions] = useState>([]); + const [percentileMap, setPercentileMap] = useState<{ [traceGroup: string]: number[] }>({}); + const [redirect, setRedirect] = useState(true); + + useEffect(() => { + setLoading(true) + const timeFilterDSL = filtersToDsl([], '', startTime, endTime); + const latencyTrendStartTime = dateMath + .parse(endTime) + ?.subtract(24, 'hours') + .toISOString()!; + const latencyTrendDSL = filtersToDsl( + filters, + query, + latencyTrendStartTime, + endTime + ); + handleDashboardRequest( + http, + dslService, + timeFilterDSL, + latencyTrendDSL, + traceItems, + setTraceItems, + setPercentileMap + ).then(() => setLoading(false)); + setRedirect(false); + }, []) + + useEffect (() => { + const toOptions = traceItems.map((item: any) => { return { label: item.dashboard_trace_group_name }}); + setTraceOptions(toOptions); + }, [traceItems]) + + useEffect (() => { + const filteredOptions = filters.filter(f => f.field === 'traceGroup').map((f) => { return { label: f.value }}); + const noDups = filteredOptions.filter((t, index) => { return filteredOptions.findIndex(trace => trace.label === t.label) === index }); + setSelectedTraces(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onTraceChange = (selectedTraces: any) => { + const traceFilters = selectedTraces.map((option: optionType) => { + return { + field: 'traceGroup', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonTraceFilters = filters.filter((f) => f.field !== 'traceGroup'); + setFilters([...nonTraceFilters, ...traceFilters]); + }; + + const onCreateTrace = (searchValue: string, flattenedOptions: any) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + if (!normalizedSearchValue) { + return; + } + const newTraceOption = { + label: searchValue + } + const newTraceFilter = { + field: 'traceGroup', + operator: 'is', + value: searchValue, + inverted: false, + disabled: false + }; + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + (option: optionType) => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + setTraceOptions([...traceOptions, newTraceOption]); + } + // Select the option. + setFilters([...filters, newTraceFilter]); + }; + + const addPercentileFilter = (condition = 'gte', additionalFilters = [] as FilterType[]) => { + if (traceItems.length === 0 || Object.keys(percentileMap).length === 0) return; + for (let i = 0; i < props.filters.length; i++) { + if (props.filters[i].custom) { + const newFilter = JSON.parse(JSON.stringify(props.filters[i])); + newFilter.custom.query.bool.should.forEach((should: any) => + should.bool.must.forEach((must: any) => { + const range = must?.range?.['traceGroupFields.durationInNanos']; + if (range) { + const duration = range.lt || range.lte || range.gt || range.gte; + if (duration || duration === 0) { + must.range['traceGroupFields.durationInNanos'] = { + [condition]: duration, + }; + } + } + }) + ); + newFilter.value = condition === 'gte' ? '>= 95th' : '< 95th'; + const newFilters = [...filters, ...additionalFilters]; + newFilters.splice(i, 1, newFilter); + setFilters(newFilters); + return; + } + } + } + + const clearTraces = () => { + const withoutTraces = filters.filter((f) => f.field !== 'traceGroup') + setFilters(withoutTraces); + }; + + return ( + + +

+ Trace Groups {selectedTraces.length} +

+
+ + + Constrain your application to specific trace groups + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setTraceOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/configuration.tsx b/dashboards-observability/public/components/application_analytics/components/configuration.tsx new file mode 100644 index 000000000..0cf800892 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/configuration.tsx @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle +} from '@elastic/eui'; +import React from 'react'; + +const dummy = [{ + level: "Available", + definition: "error rate below or equal to 1%", + id: "1" +}]; + +const dummyLogSources = [ + {logName: "index_1"}, {logName: "ingest_logs_all"} +]; + +const dummyServicesEntities = [ + {serviceName: "Payment"}, {serviceName: "Users"}, {serviceName: "Purchase"} +]; + +const dummyTraceGroups = [ + {traceGroup: "Payment.auto"}, {traceGroup: "Users.admin"}, {traceGroup: "Purchase.source"} +]; + +export const Configuration = () => { + + const tableColumns = [ + { + field: 'level', + name: 'Level', + render: (value) => value, + }, + { + field: 'definition', + name: 'Definition', + render: (value) => value, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + level: string; + id: string; + definition: string; + }> + >; + + return ( +
+ + + + + + +

+ Composition +

+
+
+ + + + {}}> + Edit composition + + + + +
+ + + + +
Log Sources
+
    + {dummyLogSources.map(function(item, index){ + return
  • {item.logName}
  • + })} +
+
+
+ + +
Services & Entities
+
    + {dummyServicesEntities.map(function(item, index){ + return
  • {item.serviceName}
  • + })} +
+
+
+ + +
Trace groups
+
    + {dummyTraceGroups.map(function(item, index){ + return
  • {item.traceGroup}
  • + })} +
+
+
+
+
+
+
+ + + + + + +

+ Availability +

+
+
+ + + + {}}> + Edit availability + + + + +
+ + +
+
+
+
+ ) +} diff --git a/dashboards-observability/public/components/application_analytics/components/create.tsx b/dashboards-observability/public/components/application_analytics/components/create.tsx new file mode 100644 index 000000000..f71389778 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/create.tsx @@ -0,0 +1,171 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTitle, + EuiToolTip +} from "@elastic/eui"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { ChangeEvent } from "react"; +import { AppAnalyticsComponentDeps } from "../home"; +import { TraceConfig } from './config_components/trace_config'; +import { ServiceConfig } from "./config_components/service_config"; +import { LogConfig } from "./config_components/log_config"; +import { PPLReferenceFlyout } from "../../../components/common/helpers"; +import { optionType } from "common/constants/application_analytics"; + +interface CreateAppProps extends AppAnalyticsComponentDeps { + dslService: DSLService; +}; + +export const CreateApp = (props: CreateAppProps) => { + const { parentBreadcrumb, chrome, query } = props; + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [selectedServices, setSelectedServices] = useState>([]); + const [selectedTraces, setSelectedTraces] = useState>([]); + const [state, setState] = useState({ + name: '', + description: '' + }); + + useEffect(() => { + chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: 'Create', + href: '#/application_analytics/create', + }, + ]); + }, []) + + const closeFlyout = () => { + setIsFlyoutVisible(false); + }; + + let flyout; + if (isFlyoutVisible) { + flyout = ; + } + + const onChange = (e: ChangeEvent) => { + setState({ + ...state, + [e.target.name]: e.target.value + }); + }; + + const isDisabled = !state.name || !query || !selectedTraces.length || !selectedServices.length; + + const missingField = () => { + if (isDisabled) { + let popoverContent = ''; + if (!state.name) { + popoverContent = 'Name is required.' + } else if (!query) { + popoverContent = 'Log Source is required.' + } else if (!selectedServices.length) { + popoverContent = 'Services & Entities is required.' + } else if (!selectedTraces.length) { + popoverContent = 'Trace Groups are required.' + } + return

{popoverContent}

; + } + }; + + return ( +
+ + + + + +

Create application

+
+
+
+ + + + +

Application information

+
+
+
+ + + + onChange(e)} + /> + + + onChange(e)} + /> + + +
+ + + + + +

Composition

+
+
+
+ + + + + + +
+ + + + + Cancel + + + + + + Create + + + + +
+
+ {flyout} +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx new file mode 100644 index 000000000..c9d54f9c2 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; + +/* The file contains helper functions for modal layouts + * getDeleteModal - returns a confirm-modal with clear option + */ + +export const getClearModal = ( + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void, + onConfirm: (event?: React.MouseEvent) => void, + title: string, + message: string, + confirmMessage?: string +) => { + return ( + + + {message} + + + ); +}; \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/home.tsx b/dashboards-observability/public/components/application_analytics/home.tsx new file mode 100644 index 000000000..85d7d8466 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/home.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +import React, { useEffect, useState } from 'react'; +import { AppTable } from './components/app_table'; +import { Application } from './components/application'; +import { CreateApp } from './components/create' +import { Route, RouteComponentProps, Switch } from 'react-router'; +import { TraceAnalyticsComponentDeps, TraceAnalyticsCoreDeps } from '../trace_analytics/home'; +import { FilterType } from '../trace_analytics/components/common/filters/filters'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import { handleIndicesExistRequest } from '../trace_analytics/requests/request_handler'; +import { ObservabilitySideBar } from '../common/side_nav'; +import { NotificationsStart } from '../../../../../src/core/public'; + +export interface AppAnalyticsCoreDeps extends TraceAnalyticsCoreDeps {} + +interface HomeProps extends RouteComponentProps, AppAnalyticsCoreDeps { + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + +export interface AppAnalyticsComponentDeps extends TraceAnalyticsComponentDeps {} + +export type ApplicationType = { + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + dateCreated: string; + dateModified: string; +}; + +const dateString = new Date().toISOString(); + +const dummyApplication: ApplicationType[] = [{ + name: "Cool Application", + id: "id", + composition: "Payment, user_db", + currentAvailability: "Available", + availabilityMetrics: "Error rate: 0.80%, Throughput: 0.94%, Latency: 600ms", + dateCreated: dateString, + dateModified: dateString +}]; + +export const Home = (props: HomeProps) => { + const { pplService, dslService, timestampUtils, savedObjects, parentBreadcrumb, http, chrome, notifications } = props; + const [indicesExist, setIndicesExist] = useState(true); + const storedFilters = sessionStorage.getItem('AppAnalyticsFilters'); + const [query, setQuery] = useState(sessionStorage.getItem('AppAnalyticsQuery') || ''); + const [filters, setFilters] = useState( + storedFilters ? JSON.parse(storedFilters) : [] + ); + const [startTime, setStartTime] = useState( + sessionStorage.getItem('AppAnalyticsStartTime') || 'now-24h' + ); + const [endTime, setEndTime] = useState( + sessionStorage.getItem('AppAnalyticsEndTime') || 'now' + ); + + const setFiltersWithStorage = (newFilters: FilterType[]) => { + setFilters(newFilters); + sessionStorage.setItem('AppAnalyticsFilters', JSON.stringify(newFilters)); + }; + const setQueryWithStorage = (newQuery: string) => { + setQuery(newQuery); + sessionStorage.setItem('AppAnalyticsQuery', newQuery); + }; + const setStartTimeWithStorage = (newStartTime: string) => { + setStartTime(newStartTime); + sessionStorage.setItem('AppAnalyticsStartTime', newStartTime); + }; + const setEndTimeWithStorage = (newEndTime: string) => { + setEndTime(newEndTime); + sessionStorage.setItem('AppAnalyticsEndTime', newEndTime); + }; + + useEffect(() => { + handleIndicesExistRequest(http, setIndicesExist); + }, []); + + const commonProps: AppAnalyticsComponentDeps = { + parentBreadcrumb: parentBreadcrumb, + http: http, + chrome: chrome, + query, + setQuery: setQueryWithStorage, + filters, + setFilters: setFiltersWithStorage, + startTime, + setStartTime: setStartTimeWithStorage, + endTime, + setEndTime: setEndTimeWithStorage, + indicesExist, + }; + + return ( +
+ + + + + + } + /> + + + } + /> + + + } + /> + +
+ ) +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete.test.tsx b/dashboards-observability/public/components/common/search/autocomplete.test.tsx index e5b740d07..94b3ecdfb 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.test.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.test.tsx @@ -37,7 +37,7 @@ describe('renders autocomplete', function () { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); it('handles query change', () => { act(() => { diff --git a/dashboards-observability/public/components/common/search/autocomplete.tsx b/dashboards-observability/public/components/common/search/autocomplete.tsx index a42b47ce7..410bcd9c6 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.tsx @@ -106,7 +106,7 @@ export const Autocomplete = (props: IQueryBarProps) => { {...autocomplete.getInputProps({ id: 'autocomplete-textarea', "data-test-subj": "searchAutocompleteTextArea", - placeholder: 'Enter PPL query to retrieve logs', + placeholder: 'Enter PPL query', inputElement: null })} /> diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.ts b/dashboards-observability/public/components/common/search/autocomplete_logic.ts new file mode 100644 index 000000000..09b1c58a5 --- /dev/null +++ b/dashboards-observability/public/components/common/search/autocomplete_logic.ts @@ -0,0 +1,267 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getDataValueQuery } from './queries/data_queries'; +import DSLService from 'public/services/requests/dsl'; +import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; + +let currIndex: string = ''; +let currField: string = ''; +let currFieldType: string = ''; + +let inFieldsCommaLoop: boolean = false; +let inMatch: boolean = false; +let nextWhere: number = Number.MAX_SAFE_INTEGER; +let nextStats: number = Number.MAX_SAFE_INTEGER; + +const indexList: string[] = []; +const fieldList: string[] = []; +const fieldsFromBackend: fieldItem[] = []; +const indicesFromBackend: indexItem[] = []; +const dataValuesFromBackend: dataItem[] = []; + +const getIndices = async (dslService: DSLService): Promise => { + if (indicesFromBackend.length === 0) { + const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); + for (let i = 0; i < indices.length; i++) { + indicesFromBackend.push({ + label: indices[i].index, + }); + indexList.push(indices[i].index); + } + } +}; + +const getFields = async (dslService: DSLService): Promise => { + if (currIndex !== '') { + const res = await dslService.fetchFields(currIndex); + fieldsFromBackend.length = 0; + for (const element in res?.[currIndex].mappings.properties) { + if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else { + fieldsFromBackend.push({ + label: element, + type: res?.[currIndex].mappings.properties[element].type, + }); + } + fieldList.push(element); + } + } +}; + +const getDataValues = async ( + index: string, + field: string, + fieldType: string, + dslService: DSLService +): Promise => { + const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; + dataValuesFromBackend.length = 0; + res.forEach((e: any) => { + if (fieldType === 'string') { + dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); + } else if (fieldType === 'boolean') { + if (e.key === 1) { + dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); + } else { + dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); + } + } else if (fieldType !== 'geo_point') { + dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); + } + }); + return dataValuesFromBackend; +}; + +export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { + if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { + currIndex = item.itemName; + getFields(dslService); + } + setQuery(item.label + ' '); +}; + +// Function to create the array of objects to be suggested +const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { + const lowerWord = word.toLowerCase(); + const filteredList = items.filter( + (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) + ); + const suggestionList = []; + for (let i = 0; i < filteredList.length; i++) { + suggestionList.push({ + label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, + input: str, + suggestion: filteredList[i].label.substring(word.length), + itemName: filteredList[i].label, + }); + } + return suggestionList; +}; + +// Function for the first command in query, also needs to get available indices +const getFirstPipe = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + getIndices(dslService); + return fillSuggestions(str, prefix, firstCommand); +}; + +// Main logic behind autocomplete (Based on most recent inputs) +export const getSuggestions = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + const lowerPrefix = prefix.toLowerCase(); + const fullSuggestions: AutocompleteItem[] = []; + + // Check the last full word in the query, then suggest inputs based off that + if (splittedModel.length === 1) { + currField = ''; + currIndex = ''; + return getFirstPipe(str, dslService); + } else if (splittedModel.length > 1) { + if (splittedModel[splittedModel.length - 2] === '|') { + inFieldsCommaLoop = false; + inMatch = false; + nextWhere = Number.MAX_SAFE_INTEGER; + nextStats = Number.MAX_SAFE_INTEGER; + currField = ''; + currFieldType = ''; + return fillSuggestions(str, prefix, pipeCommands); + } else if (splittedModel[splittedModel.length - 2].includes(',')) { + if (inFieldsCommaLoop) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + if (inMatch) { + inMatch = true; + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } + return fullSuggestions; + } else if ( + splittedModel[splittedModel.length - 2] === 'source' + ) { + return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if ( + (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') + ) { + return fillSuggestions(str, prefix, indicesFromBackend); + } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { + currIndex = splittedModel[splittedModel.length - 2]; + getFields(dslService); + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (splittedModel[splittedModel.length - 2] === 'stats') { + nextStats = splittedModel.length; + return fillSuggestions(str, prefix, statsCommands); + } else if (nextStats === splittedModel.length - 1) { + if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { + if (splittedModel[splittedModel.length - 2] === 'count()') { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + const numberFields = fieldsFromBackend.filter( + (field: { label: string, type: string }) => + field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) + ); + for (let i = 0; i < numberFields.length; i++) { + var field: {label: string} = numberFields[i]; + fullSuggestions.push({ + label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', + input: str, + suggestion: field.label.substring(prefix.length) + ' )', + itemName: field.label + ' )', + }); + } + return fullSuggestions; + } + } + } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextStats === splittedModel.length - 3) { + if (splittedModel[splittedModel.length - 3] === 'by') { + return [ + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } + } else if (nextStats === splittedModel.length - 4) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + else if (splittedModel[splittedModel.length - 2] === 'fields') { + inFieldsCommaLoop = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'dedup') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'where') { + nextWhere = splittedModel.length; + return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); + } else if (splittedModel[splittedModel.length - 2] === 'match(') { + inMatch = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextWhere === splittedModel.length - 1) { + fullSuggestions.push({ + label: str + '=', + input: str, + suggestion: '=', + itemName: '=', + }); + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); + } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } else if (nextWhere === splittedModel.length - 2) { + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (inFieldsCommaLoop) { + return [ + { + label: str.substring(0, str.length - 1) + ',', + input: str.substring(0, str.length - 1), + suggestion: ',', + itemName: ',', + }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' }, + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); + } else if (inMatch) { + inMatch = false; + return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } + return []; + } +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx b/dashboards-observability/public/components/common/search/autocomplete_logic.tsx deleted file mode 100644 index 375bd77dd..000000000 --- a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getDataValueQuery } from './queries/data_queries'; -import DSLService from 'public/services/requests/dsl'; -import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; - -let currIndex: string = ''; -let currField: string = ''; -let currFieldType: string = ''; - -let inFieldsCommaLoop: boolean = false; -let inMatch: boolean = false; -let nextWhere: number = Number.MAX_SAFE_INTEGER; -let nextStats: number = Number.MAX_SAFE_INTEGER; - -const indexList: string[] = []; -const fieldList: string[] = []; -const fieldsFromBackend: fieldItem[] = []; -const indicesFromBackend: indexItem[] = []; -const dataValuesFromBackend: dataItem[] = []; - -const getIndices = async (dslService: DSLService): Promise => { - if (indicesFromBackend.length === 0) { - const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); - for (let i = 0; i < indices.length; i++) { - indicesFromBackend.push({ - label: indices[i].index, - }); - indexList.push(indices[i].index); - } - } - }; - - const getFields = async (dslService: DSLService): Promise => { - if (currIndex !== '') { - const res = await dslService.fetchFields(currIndex); - fieldsFromBackend.length = 0; - for (const element in res?.[currIndex].mappings.properties) { - if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else { - fieldsFromBackend.push({ - label: element, - type: res?.[currIndex].mappings.properties[element].type, - }); - } - fieldList.push(element); - } - } - }; - - const getDataValues = async ( - index: string, - field: string, - fieldType: string, - dslService: DSLService - ): Promise => { - const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; - dataValuesFromBackend.length = 0; - res.forEach((e: any) => { - if (fieldType === 'string') { - dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); - } else if (fieldType === 'boolean') { - if (e.key === 1) { - dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); - } else { - dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); - } - } else if (fieldType !== 'geo_point') { - dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); - } - }); - return dataValuesFromBackend; - }; - -export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { - if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { - currIndex = item.itemName; - getFields(dslService); - } - setQuery(item.label + ' '); - }; - -// Function to create the array of objects to be suggested - const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { - const lowerWord = word.toLowerCase(); - const filteredList = items.filter( - (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) - ); - const suggestionList = []; - for (let i = 0; i < filteredList.length; i++) { - suggestionList.push({ - label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, - input: str, - suggestion: filteredList[i].label.substring(word.length), - itemName: filteredList[i].label, - }); - } - return suggestionList; - }; - - // Function for the first command in query, also needs to get available indices - const getFirstPipe = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - getIndices(dslService); - return fillSuggestions(str, prefix, firstCommand); - }; - - // Main logic behind autocomplete (Based on most recent inputs) - export const getSuggestions = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - const lowerPrefix = prefix.toLowerCase(); - const fullSuggestions: AutocompleteItem[] = []; - - // Check the last full word in the query, then suggest inputs based off that - if (splittedModel.length === 1) { - currField = ''; - currIndex = ''; - return getFirstPipe(str, dslService); - } else if (splittedModel.length > 1) { - if (splittedModel[splittedModel.length - 2] === '|') { - inFieldsCommaLoop = false; - inMatch = false; - nextWhere = Number.MAX_SAFE_INTEGER; - nextStats = Number.MAX_SAFE_INTEGER; - currField = ''; - currFieldType = ''; - return fillSuggestions(str, prefix, pipeCommands); - } else if (splittedModel[splittedModel.length - 2].includes(',')) { - if (inFieldsCommaLoop) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - if (inMatch) { - inMatch = true; - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } - return fullSuggestions; - } else if ( - splittedModel[splittedModel.length - 2] === 'source' - ) { - return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if ( - (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') - ) { - return fillSuggestions(str, prefix, indicesFromBackend); - } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { - currIndex = splittedModel[splittedModel.length - 2]; - getFields(dslService); - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (splittedModel[splittedModel.length - 2] === 'stats') { - nextStats = splittedModel.length; - return fillSuggestions(str, prefix, statsCommands); - } else if (nextStats === splittedModel.length - 1) { - if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { - if (splittedModel[splittedModel.length - 2] === 'count()') { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - const numberFields = fieldsFromBackend.filter( - (field: { label: string, type: string }) => - field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) - ); - for (let i = 0; i < numberFields.length; i++) { - var field: {label: string} = numberFields[i]; - fullSuggestions.push({ - label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', - input: str, - suggestion: field.label.substring(prefix.length) + ' )', - itemName: field.label + ' )', - }); - } - return fullSuggestions; - } - } - } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextStats === splittedModel.length - 3) { - if (splittedModel[splittedModel.length - 3] === 'by') { - return [ - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } - } else if (nextStats === splittedModel.length - 4) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - else if (splittedModel[splittedModel.length - 2] === 'fields') { - inFieldsCommaLoop = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'dedup') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'where') { - nextWhere = splittedModel.length; - return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); - } else if (splittedModel[splittedModel.length - 2] === 'match(') { - inMatch = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextWhere === splittedModel.length - 1) { - fullSuggestions.push({ - label: str + '=', - input: str, - suggestion: '=', - itemName: '=', - }); - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); - } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } else if (nextWhere === splittedModel.length - 2) { - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (inFieldsCommaLoop) { - return [ - { - label: str.substring(0, str.length - 1) + ',', - input: str.substring(0, str.length - 1), - suggestion: ',', - itemName: ',', - }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' }, - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); - } else if (inMatch) { - inMatch = false; - return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } - return []; - } - }; - \ No newline at end of file diff --git a/dashboards-observability/public/components/common/search/search.test.tsx b/dashboards-observability/public/components/common/search/search.test.tsx index 37c934386..74dcd1a0a 100644 --- a/dashboards-observability/public/components/common/search/search.test.tsx +++ b/dashboards-observability/public/components/common/search/search.test.tsx @@ -48,7 +48,7 @@ describe('Search bar', () => { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); fireEvent.change(searchBar, { target: { value: 'new query' } }); expect(handleQueryChange).toBeCalledWith('new query'); }); diff --git a/dashboards-observability/public/components/common/side_nav.tsx b/dashboards-observability/public/components/common/side_nav.tsx index 83129f315..c00a3e763 100644 --- a/dashboards-observability/public/components/common/side_nav.tsx +++ b/dashboards-observability/public/components/common/side_nav.tsx @@ -32,7 +32,7 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { // Default page is Events Analytics // But it is kept as second option in side nav if (hash === '#/') { - items[0].items[1].isSelected = true; + items[0].items[2].isSelected = true; return true; } for (let i = 0; i < items.length; i++) { @@ -51,6 +51,11 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { name: 'Observability', id: 0, items: [ + { + name: 'Application analytics', + id: 1, + href: '#/application_analytics', + }, { name: 'Trace analytics', id: 1, diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 3b4d2d569..185fc3beb 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -28,7 +28,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { CSSProperties, ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE, @@ -40,12 +40,7 @@ import moment from 'moment'; import _ from 'lodash'; import { CustomPanelListType } from '../../../common/types/custom_panels'; import { getSampleDataModal } from '../common/helpers/add_sample_modal'; - -const pageStyles: CSSProperties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../common/constants/shared'; /* * "CustomPanelTable" module, used to view all the saved panels diff --git a/dashboards-observability/public/components/explorer/event_analytics.tsx b/dashboards-observability/public/components/explorer/event_analytics.tsx index 248bdcacd..2b90c152d 100644 --- a/dashboards-observability/public/components/explorer/event_analytics.tsx +++ b/dashboards-observability/public/components/explorer/event_analytics.tsx @@ -5,6 +5,7 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { EmptyTabParams } from 'common/types/explorer'; import { isEmpty } from 'lodash'; import React, { ReactChild, useState } from 'react'; import { HashRouter, Route, Switch, useHistory } from 'react-router-dom'; @@ -37,10 +38,10 @@ export const EventAnalytics = ({ setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); }; - const getExistingEmptyTab = ({ tabIds, queries, explorerData }) => { + const getExistingEmptyTab = ({ tabIds, queries, explorerData }: EmptyTabParams) => { let emptyTabId = ''; - for (let i = 0; i < tabIds.length; i++) { - const tid = tabIds[i]; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { emptyTabId = tid; break; @@ -80,7 +81,6 @@ export const EventAnalytics = ({ timestampUtils={timestampUtils} http={http} setToast={setToast} - chrome={chrome} getExistingEmptyTab={getExistingEmptyTab} history={history} notifications={notifications} @@ -106,10 +106,9 @@ export const EventAnalytics = ({ http={http} savedObjects={savedObjects} dslService={dslService} - timestampUtils={timestampUtils} + pplService={pplService} setToast={setToast} getExistingEmptyTab={getExistingEmptyTab} - history={history} /> ); diff --git a/dashboards-observability/public/components/explorer/explorer.tsx b/dashboards-observability/public/components/explorer/explorer.tsx index 3aa13d7e0..2eabe2637 100644 --- a/dashboards-observability/public/components/explorer/explorer.tsx +++ b/dashboards-observability/public/components/explorer/explorer.tsx @@ -72,7 +72,8 @@ export const Explorer = ({ setToast, history, notifications, - savedObjectId + savedObjectId, + tabCreatedTypes }: IExplorerProps) => { const dispatch = useDispatch(); const requestParams = { tabId, }; diff --git a/dashboards-observability/public/components/explorer/home.tsx b/dashboards-observability/public/components/explorer/home.tsx index d2e1ac5b4..c7b15d287 100644 --- a/dashboards-observability/public/components/explorer/home.tsx +++ b/dashboards-observability/public/components/explorer/home.tsx @@ -86,8 +86,8 @@ export const Home = (props: IHomeProps) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedDateRange, setSelectedDateRange] = useState>(['now-15m', 'now']); - const [savedHistories, setSavedHistories] = useState([]); - const [selectedHisotries, setSelectedHisotries] = useState([]); + const [savedHistories, setSavedHistories] = useState>([]); + const [selectedHistories, setSelectedHistories] = useState>([]); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isTableLoading, setIsTableLoading] = useState(false); const [modalLayout, setModalLayout] = useState(); @@ -113,7 +113,7 @@ export const Home = (props: IHomeProps) => { }; const deleteHistoryList = async () => { - const objectIdsToDelete = selectedHisotries.map((history) => history.data.objectId); + const objectIdsToDelete = selectedHistories.map((history) => history.data.objectId); await savedObjects .deleteSavedObjectsList({ objectIdList: objectIdsToDelete }) .then(async (res) => { @@ -264,7 +264,7 @@ export const Home = (props: IHomeProps) => { }); }); setToast(`Sample events added successfully.`); - } catch (error) { + } catch (error: any) { setToast(`Cannot add sample events data, error: ${error}`, 'danger'); console.error(error.body.message); } finally { @@ -284,13 +284,13 @@ export const Home = (props: IHomeProps) => { ); const deleteHistory = () => { - const customPanelString = `${selectedHisotries.length > 1 ? 'histories' : 'history'}`; + const customPanelString = `${selectedHistories.length > 1 ? 'histories' : 'history'}`; setModalLayout( ); showModal(); @@ -299,7 +299,7 @@ export const Home = (props: IHomeProps) => { const popoverItems: ReactElement[] = [ { setIsActionsPopoverOpen(false); deleteHistory(); @@ -408,8 +408,8 @@ export const Home = (props: IHomeProps) => { savedHistories={savedHistories} handleHistoryClick={handleHistoryClick} isTableLoading={isTableLoading} - handleSelectHistory={setSelectedHisotries} - selectedHisotries={selectedHisotries} + handleSelectHistory={setSelectedHistories} + selectedHistories={selectedHistories} /> ) : ( <> diff --git a/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx index 50802c13b..0c3abcad3 100644 --- a/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx +++ b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx @@ -16,6 +16,7 @@ interface savedQueryTableProps { handleHistoryClick: (objectId: string) => void; handleSelectHistory: (selectedHistories: Array) => void; isTableLoading: boolean; + selectedHistories: Array; } export function SavedQueryTable({ @@ -26,9 +27,9 @@ export function SavedQueryTable({ }: savedQueryTableProps) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); - const pageIndexRef = useRef(); + const pageIndexRef = useRef(); pageIndexRef.current = pageIndex; - const pageSizeRef = useRef(); + const pageSizeRef = useRef(); pageSizeRef.current = pageSize; const onTableChange = ({ page = {} }) => { @@ -44,7 +45,7 @@ export function SavedQueryTable({ name: '', sortable: true, width: '40px', - render: (item) => { + render: (item: any) => { if (item == 'Visualization') { return (
@@ -66,7 +67,7 @@ export function SavedQueryTable({ width: '70%', sortable: true, truncateText: true, - render: (item) => { + render: (item: any) => { return ( { diff --git a/dashboards-observability/public/components/explorer/log_explorer.tsx b/dashboards-observability/public/components/explorer/log_explorer.tsx index 91a9578c1..4d8cf3a07 100644 --- a/dashboards-observability/public/components/explorer/log_explorer.tsx +++ b/dashboards-observability/public/components/explorer/log_explorer.tsx @@ -36,6 +36,7 @@ export const LogExplorer = ({ getExistingEmptyTab, history, notifications, + http }: ILogExplorerProps) => { const dispatch = useDispatch(); @@ -178,17 +179,18 @@ export const LogExplorer = ({ <> + tabCreatedTypes={tabCreatedTypes} + http={http} + /> ) }; } diff --git a/dashboards-observability/public/components/notebooks/components/note_table.tsx b/dashboards-observability/public/components/notebooks/components/note_table.tsx index 8d38bcbe4..c3810c11c 100644 --- a/dashboards-observability/public/components/notebooks/components/note_table.tsx +++ b/dashboards-observability/public/components/notebooks/components/note_table.tsx @@ -27,7 +27,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import CSS from 'csstype'; import _ from 'lodash'; import moment from 'moment'; import React, { ReactElement, useEffect, useState } from 'react'; @@ -43,12 +42,7 @@ import { getSampleNotebooksModal, } from './helpers/modal_containers'; import { NotebookType } from './main'; - -const pageStyles: CSS.Properties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../../common/constants/shared'; type NoteTableProps = { loading: boolean; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx index 15eaf5a33..1410bfea0 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx @@ -13,22 +13,23 @@ import { import _ from 'lodash'; import React from 'react'; -const getFields = (page: 'dashboard' | 'traces' | 'services') => +const getFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => ({ dashboard: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], traces: ['traceId', 'traceGroup', 'serviceName', 'error', 'status.message', 'latency'], services: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], + app: ['traceId', 'traceGroup', 'serviceName'], }[page]); // filters will take effect and can be manually added -export const getFilterFields = (page: 'dashboard' | 'traces' | 'services') => getFields(page); +export const getFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => getFields(page); // filters will take effect -export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services') => { +export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => { const fields = getFields(page); if (page !== 'services') return [...fields, 'Latency percentile within trace group']; return fields; }; -const getType = (field: string): string => { +const getType = (field: string): string | null => { const typeMapping = { attributes: { host: { @@ -106,7 +107,7 @@ export const getOperatorOptions = (field: string) => { }; const operators = [ ...operatorMapping.default_first, - ...operatorMapping[type], + ..._.get(operatorMapping, type), ...operatorMapping.default_last, ]; return operators; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx index 7fdf9fed2..af3870626 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx @@ -36,7 +36,7 @@ export interface FiltersProps { } interface FiltersOwnProps extends FiltersProps { - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; } export function Filters(props: FiltersOwnProps) { diff --git a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx index ec876522e..d1dfe5a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx @@ -47,7 +47,7 @@ export interface SearchBarProps extends FiltersProps { interface SearchBarOwnProps extends SearchBarProps { refresh: () => void; - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; datepickerOnly?: boolean; } diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx index e306f0458..58c569d45 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx @@ -23,6 +23,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); @@ -59,6 +60,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx index 642b56353..20a504a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx @@ -22,9 +22,14 @@ import { ThroughputPlt } from '../common/plots/throughput_plt'; import { SearchBar } from '../common/search_bar'; import { DashboardTable } from './dashboard_table'; -interface DashboardProps extends TraceAnalyticsComponentDeps {} +interface DashboardProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Dashboard(props: DashboardProps) { + const { appId, appName, page, parentBreadcrumb } = props; const [tableItems, setTableItems] = useState([]); const [throughputPltItems, setThroughputPltItems] = useState({ items: [], fixedInterval: '1h' }); const [errorRatePltItems, setErrorRatePltItems] = useState({ items: [], fixedInterval: '1h' }); @@ -34,19 +39,35 @@ export function Dashboard(props: DashboardProps) { const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, + const breadCrumbs = page === 'app' ? + [ { - text: 'Trace analytics', - href: '#/trace_analytics/home', + text: 'Application analytics', + href: '#/application_analytics', }, { - text: 'Dashboards', - href: '#/trace_analytics/home', + text: `${appName}`, + href: `#/application_analytics/${appId}`, }, + ] : [ + { + text: 'Trace analytics', + href: '#/trace_analytics/home', + }, + { + text: 'Dashboards', + href: '#/trace_analytics/home', + }, + ] + + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + ...breadCrumbs ]); - const validFilters = getValidFilterFields('dashboard'); + const validFilters = getValidFilterFields(page); props.setFilters([ ...props.filters.map((filter) => ({ ...filter, @@ -156,9 +177,13 @@ export function Dashboard(props: DashboardProps) { return ( <> + {page === 'app' ? + + :

Dashboard

+ } {props.indicesExist ? ( @@ -181,6 +206,7 @@ export function Dashboard(props: DashboardProps) { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={loading} + page="dashboard" /> diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx index 5fe49c4e2..a334868e8 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx @@ -33,6 +33,7 @@ export function DashboardTable(props: { addPercentileFilter: (condition?: 'gte' | 'lte', additionalFilters?: FilterType[]) => void; setRedirect: (redirect: boolean) => void; loading: boolean; + page: 'dashboard' | 'app'; }) { const getVarianceProps = (items: any[]) => { if (items.length === 0) { @@ -317,7 +318,7 @@ export function DashboardTable(props: { ), align: 'right', sortable: true, - render: (item, row) => ( + render: props.page === 'dashboard' ? (item, row) => ( { @@ -334,7 +335,7 @@ export function DashboardTable(props: { > - ), + ) : (item) => item }, ] as Array>; @@ -371,7 +372,7 @@ export function DashboardTable(props: { }; const varianceProps = useMemo(() => getVarianceProps(props.items), [props.items]); - const columns = useMemo(() => getColumns(), [props.items]); + const columns = useMemo(() => getColumns(), [props.items, props.filters]); const titleBar = useMemo(() => renderTitleBar(props.items?.length), [props.items]); const [sorting, setSorting] = useState<{ sort: PropertySort }>({ diff --git a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx index 706e618d5..e39fd4cc1 100644 --- a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx @@ -13,17 +13,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { ServicesTable } from './services_table'; -interface ServicesProps extends TraceAnalyticsComponentDeps {} +interface ServicesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Services(props: ServicesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -31,6 +44,12 @@ export function Services(props: ServicesProps) { text: 'Services', href: '#/trace_analytics/services', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('services'); props.setFilters([ @@ -71,9 +90,13 @@ export function Services(props: ServicesProps) { return ( <> + {page==='app' ? + + :

Services

+ } { endTime="now" setEndTime={setEndTime} indicesExist={false} + page="traces" /> ); @@ -59,6 +59,7 @@ describe('Traces component', () => { endTime="now" setEndTime={setEndTime} indicesExist={true} + page="traces" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx index f06e48d86..bfe159bc9 100644 --- a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx @@ -12,17 +12,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { TracesTable } from './traces_table'; -interface TracesProps extends TraceAnalyticsComponentDeps {} +interface TracesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'traces' | 'app'; +} export function Traces(props: TracesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -30,6 +43,12 @@ export function Traces(props: TracesProps) { text: 'Traces', href: '#/trace_analytics/traces', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('traces'); props.setFilters([ @@ -55,9 +74,13 @@ export function Traces(props: TracesProps) { return ( <> + {page === 'app' ? + + :

Traces

+ } diff --git a/dashboards-observability/public/components/trace_analytics/home.tsx b/dashboards-observability/public/components/trace_analytics/home.tsx index ea198c8e7..f68e0edce 100644 --- a/dashboards-observability/public/components/trace_analytics/home.tsx +++ b/dashboards-observability/public/components/trace_analytics/home.tsx @@ -88,7 +88,7 @@ export const Home = (props: HomeProps) => { path={['/trace_analytics', '/trace_analytics/home']} render={(routerProps) => ( - + )} /> @@ -97,7 +97,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/traces" render={(routerProps) => ( - + )} /> @@ -117,7 +117,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/services" render={(routerProps) => ( - + )} /> diff --git a/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts b/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts new file mode 100644 index 000000000..e69de29bb diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 9a6c29c81..063ed3d01 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -262,7 +262,11 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" +<<<<<<< HEAD versions = ["1.1.0","1.2.2-SNAPSHOT"] +======= + versions = ["1.1.0","1.2.1-SNAPSHOT"] +>>>>>>> b52bf65 (Application Analytics (#299)) numberOfNodes = 3 plugin(provider(new Callable(){ @Override diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt new file mode 100644 index 000000000..d9f5f688d --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt @@ -0,0 +1,270 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Application main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "name": "Cool Application",
+ *   "description": "Application that includes multiple cool services",
+ *   "baseQuery": "source = opensearch_sample_database_flights",
+ *   "servicesEntities": [
+ *       "Payment",
+ *       "Users",
+ *       "Purchase"
+ *   ],
+ *   "traceGroups": [
+ *       "Payment.auto",
+ *       "Users.admin",
+ *       "Purchase.source"
+ *   ],
+ *   "availabilityLevels": [
+ *       {
+ *           "label": "Unavailable",
+ *           "color": "#D36086",
+ *           "condition": "when errorRate() is above or equal to 2%",
+ *           "order": "0",
+ *       }
+ *   ],
+ * }
+ * }
+ */ + +internal data class Application( + val name: String?, + val description: String?, + val baseQuery: String?, + val servicesEntities: List, + val traceGroups: List, + val availabilityLevels: List +) : BaseObjectData { + + internal companion object { + private val log by logger(Application::class.java) + private const val NAME_TAG = "name" + private const val DESCRIPTION_TAG = "description" + private const val BASE_QUERY_TAG = "baseQuery" + private const val SERVICES_ENTITIES_TAG = "servicesEntities" + private const val TRACE_GROUPS_TAG = "traceGroups" + private const val AVAILABILITY_LEVELS_TAG = "availabilityLevels" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Application(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the item list from parser + * @param parser data referenced at parser + * @return created list of items + */ + private fun parseItemList(parser: XContentParser): List { + val retList: MutableList = mutableListOf() + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser) + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + retList.add(AvailabilityLevel.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create ObservabilityObject object + * @param parser data referenced at parser + * @return created ObservabilityObject object + */ + fun parse(parser: XContentParser): Application { + var name: String? = null + var description: String? = null + var baseQuery: String? = null + var servicesEntities: List = listOf() + var traceGroups: List = listOf() + var availabilityLevels: List = listOf() + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + NAME_TAG -> name = parser.text() + DESCRIPTION_TAG -> description = parser.text() + BASE_QUERY_TAG -> baseQuery = parser.text() + SERVICES_ENTITIES_TAG -> servicesEntities = parser.stringList() + TRACE_GROUPS_TAG -> traceGroups = parser.stringList() + AVAILABILITY_LEVELS_TAG -> availabilityLevels = parseItemList(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Application Skipping Unknown field $fieldName") + } + } + } + return Application(name, description, baseQuery, servicesEntities, traceGroups, availabilityLevels) + } + } + + /** + * create XContentBuilder from this object using [XContentFactory.jsonBuilder()] + * @param params XContent parameters + * @return created XContentBuilder object + */ + fun toXContent(params: ToXContent.Params = ToXContent.EMPTY_PARAMS): XContentBuilder? { + return toXContent(XContentFactory.jsonBuilder(), params) + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + name = input.readString(), + description = input.readString(), + baseQuery = input.readString(), + servicesEntities = input.readStringList(), + traceGroups = input.readStringList(), + availabilityLevels = input.readList(AvailabilityLevel.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(description) + output.writeString(baseQuery) + output.writeStringCollection(servicesEntities) + output.writeStringCollection(traceGroups) + output.writeCollection(availabilityLevels) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DESCRIPTION_TAG, description) + .fieldIfNotNull(BASE_QUERY_TAG, baseQuery) + .fieldIfNotNull(SERVICES_ENTITIES_TAG, servicesEntities) + .fieldIfNotNull(TRACE_GROUPS_TAG, traceGroups) + .fieldIfNotNull(AVAILABILITY_LEVELS_TAG, availabilityLevels) + return builder.endObject() + } + + internal data class AvailabilityLevel( + val label: String?, + val color: String?, + val condition: String?, + val order: String? + ) : BaseModel { + internal companion object { + private const val LABEL_TAG = "label" + private const val COLOR_TAG = "color" + private const val CONDITION_TAG = "condition" + private const val ORDER_TAG = "order" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { AvailabilityLevel(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Trigger object + * @param parser data referenced at parser + * @return created Trigger object + */ + fun parse(parser: XContentParser): AvailabilityLevel { + var label: String? = null + var color: String? = null + var condition: String? = null + var order: String? = null + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + LABEL_TAG -> label = parser.text() + COLOR_TAG -> color = parser.text() + CONDITION_TAG -> condition = parser.text() + ORDER_TAG -> order = parser.text() + else -> log.info("$LOG_PREFIX: AvailabilityLevel Skipping Unknown field $fieldName") + } + } + label ?: throw IllegalArgumentException("$LABEL_TAG field absent") + color ?: throw IllegalArgumentException("$COLOR_TAG field absent") + condition ?: throw IllegalArgumentException("$CONDITION_TAG field absent") + order ?: throw IllegalArgumentException("$ORDER_TAG field absent") + return AvailabilityLevel(label, color, condition, order) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + label = input.readString(), + color = input.readString(), + condition = input.readString(), + order = input.readString(), + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(label) + output.writeString(color) + output.writeString(condition) + output.writeString(order) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(LABEL_TAG, label) + .field(COLOR_TAG, color) + .field(CONDITION_TAG, condition) + .field(ORDER_TAG, order) + builder.endObject() + return builder + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt index c9b4b6ce3..4da7f24bb 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt @@ -29,6 +29,10 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.OPERATIONAL_PANEL, ObjectProperty(OperationalPanel.reader, OperationalPanel.xParser) ), + Pair( + ObservabilityObjectType.APPLICATION, + ObjectProperty(Application.reader, Application.xParser) + ), Pair( ObservabilityObjectType.TIMESTAMP, ObjectProperty(Timestamp.reader, Timestamp.xParser) @@ -54,6 +58,7 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.SAVED_QUERY -> objectData is SavedQuery ObservabilityObjectType.SAVED_VISUALIZATION -> objectData is SavedVisualization ObservabilityObjectType.OPERATIONAL_PANEL -> objectData is OperationalPanel + ObservabilityObjectType.APPLICATION -> objectData is Application ObservabilityObjectType.TIMESTAMP -> objectData is Timestamp ObservabilityObjectType.NONE -> true } diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt index c5e4cd8e8..29aa51852 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.observability.model import org.opensearch.common.io.stream.StreamInput diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt index b1940af47..91e4edcd0 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt @@ -6,6 +6,7 @@ package org.opensearch.observability.model import org.opensearch.commons.utils.EnumParser +import org.opensearch.observability.model.RestTag.APPLICATION_FIELD import org.opensearch.observability.model.RestTag.NOTEBOOK_FIELD import org.opensearch.observability.model.RestTag.OPERATIONAL_PANEL_FIELD import org.opensearch.observability.model.RestTag.SAVED_QUERY_FIELD @@ -42,6 +43,11 @@ enum class ObservabilityObjectType(val tag: String) { return tag } }, + APPLICATION(APPLICATION_FIELD) { + override fun toString(): String { + return tag + } + }, TIMESTAMP(TIMESTAMP_FIELD) { override fun toString(): String { return tag diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt index 422582080..1bc390049 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt @@ -31,6 +31,7 @@ internal object RestTag { const val SAVED_QUERY_FIELD = "savedQuery" const val SAVED_VISUALIZATION_FIELD = "savedVisualization" const val OPERATIONAL_PANEL_FIELD = "operationalPanel" + const val APPLICATION_FIELD = "application" const val TIMESTAMP_FIELD = "timestamp" private val INCLUDE_ID = Pair(OBJECT_ID_FIELD, "true") private val EXCLUDE_ACCESS = Pair(ACCESS_LIST_FIELD, "false") diff --git a/opensearch-observability/src/main/resources/observability-mapping.yml b/opensearch-observability/src/main/resources/observability-mapping.yml index 4748204b8..beb9615bb 100644 --- a/opensearch-observability/src/main/resources/observability-mapping.yml +++ b/opensearch-observability/src/main/resources/observability-mapping.yml @@ -54,6 +54,14 @@ properties: fields: keyword: type: keyword + application: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword timestamp: type: object properties: From 1fb9c9fc4948f1f9e887391b7d6b12e8d2e91491 Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 21 Dec 2021 15:46:48 -0800 Subject: [PATCH 08/16] Fix merge conflict Signed-off-by: Eugene Lee --- opensearch-observability/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 063ed3d01..9a6c29c81 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -262,11 +262,7 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" -<<<<<<< HEAD versions = ["1.1.0","1.2.2-SNAPSHOT"] -======= - versions = ["1.1.0","1.2.1-SNAPSHOT"] ->>>>>>> b52bf65 (Application Analytics (#299)) numberOfNodes = 3 plugin(provider(new Callable(){ @Override From ab3a385ad7afa0e039d9d0c845ad4ca6a54174c9 Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Wed, 22 Dec 2021 09:49:33 -0800 Subject: [PATCH 09/16] Update Workflow (#360) * updated snapshot in workflow to 1.2.3 Signed-off-by: Shenoy Pratik * replaced variable with 1.2.3-snapshot Signed-off-by: Shenoy Pratik * revert string to variable Signed-off-by: Shenoy Pratik --- .../opensearch-observability-test-and-build-workflow.yml | 2 +- opensearch-observability/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/opensearch-observability-test-and-build-workflow.yml b/.github/workflows/opensearch-observability-test-and-build-workflow.yml index 46630accf..ddfbc740b 100644 --- a/.github/workflows/opensearch-observability-test-and-build-workflow.yml +++ b/.github/workflows/opensearch-observability-test-and-build-workflow.yml @@ -3,7 +3,7 @@ name: Test and Build OpenSearch Observability Backend Plugin on: [pull_request, push] env: - OPENSEARCH_VERSION: '1.2.2-SNAPSHOT' + OPENSEARCH_VERSION: '1.2.3-SNAPSHOT' OPENSEARCH_BRANCH: '1.2' COMMON_UTILS_BRANCH: 'main' diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 9a6c29c81..7cd5c88a7 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,7 +9,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.2.2-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "1.2.3-SNAPSHOT") // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -262,7 +262,7 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" - versions = ["1.1.0","1.2.2-SNAPSHOT"] + versions = ["1.1.0",opensearch_version] numberOfNodes = 3 plugin(provider(new Callable(){ @Override From f1569f2dd1d1cb9cb2096f675257c692819c03de Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 14 Dec 2021 11:56:03 -0800 Subject: [PATCH 10/16] Application Analytics (#299) * Add database schema for application Signed-off-by: Eugene Lee * Finished front end for Application overview Signed-off-by: Eugene Lee * Finished application detail page tabs Signed-off-by: Eugene Lee * WIP: Overview Page Signed-off-by: Eugene Lee * Rough sketch of App Analytics UI Signed-off-by: Eugene Lee * Create dummy page Signed-off-by: Eugene Lee * Create app complete. Stabilizing dashboard component. Signed-off-by: Eugene Lee * Update to 1.2 Observability Signed-off-by: Eugene Lee * notebooks internal error Signed-off-by: Eugene Lee * Address comments on PR: copyright headers, indentation, unnecessary render props Signed-off-by: Eugene Lee * Set max width of app and event Signed-off-by: Eugene Lee * Remove optional after description Signed-off-by: Eugene Lee * Change to singular Signed-off-by: Eugene Lee * Remove count badge for log source Signed-off-by: Eugene Lee * #290: Change form row label to ppl base query Signed-off-by: Eugene Lee * #291: Change description and help text for log source Signed-off-by: Eugene Lee * Pass down proper props Signed-off-by: Eugene Lee * Resolve gradle error and module not found error Signed-off-by: Eugene Lee * Resolve kotlin errors Signed-off-by: Eugene Lee * Fix parsers Signed-off-by: Eugene Lee * Add praseItemList Signed-off-by: Eugene Lee * Camelcase fields Signed-off-by: Eugene Lee * Remove whitespace, add copyright Signed-off-by: Eugene Lee * #292: Add autocomplete to Log Source accordion Signed-off-by: Eugene Lee * Lexicographic kotlin import Signed-off-by: Eugene Lee * Add newline at end of files Signed-off-by: Eugene Lee * #293: Add service map to create page Signed-off-by: Eugene Lee * #304: Activate Clear All button for services Signed-off-by: Eugene Lee * #305: Add button to clear base query Signed-off-by: Eugene Lee * opensearch-project#295: Add eui combo box for trace groups Signed-off-by: Eugene Lee * Separate out configuration renders Signed-off-by: Eugene Lee * debug adding filters traces Signed-off-by: Eugene Lee * #296: Add traces table to config Signed-off-by: Eugene Lee * Change from tsx to ts Signed-off-by: Eugene Lee * opensearch-project#309: Add page props and add app specific filters Signed-off-by: Eugene Lee * #308: Add button to clear trace groups Signed-off-by: Eugene Lee * #311: Allow services and traces to be selected Signed-off-by: Eugene Lee * Remove link to traces on table Signed-off-by: Eugene Lee * Disable clear all if nothing selected Signed-off-by: Eugene Lee * disable clear all when no log source Signed-off-by: Eugene Lee * Remove comment, add style to constant, temporarily remove availability Signed-off-by: Eugene Lee * Address PR comments Signed-off-by: Eugene Lee * Revert type assignment Signed-off-by: Eugene Lee * Update tests, builds and doc (#318) * rebased with bwc tests Signed-off-by: Shenoy Pratik * updated bwc tests Signed-off-by: Shenoy Pratik * added release notes Signed-off-by: Shenoy Pratik * Fix errors and address comments Signed-off-by: Eugene Lee * #319: Disable create until required fields are filled out Signed-off-by: Eugene Lee * #329: Add missing field tool tip Signed-off-by: Eugene Lee * Remove unnecessary imports Signed-off-by: Eugene Lee * #320: Add clear modal for friction Signed-off-by: Eugene Lee Co-authored-by: Shenoy Pratik --- .../common/constants/application_analytics.ts | 19 ++ .../common/constants/shared.ts | 8 + .../common/types/explorer.ts | 6 +- .../public/components/app.tsx | 21 +- .../components/app_table.tsx | 221 ++++++++++++++ .../components/application.tsx | 244 ++++++++++++++++ .../config_components/log_config.tsx | 108 +++++++ .../config_components/service_config.tsx | 116 ++++++++ .../config_components/trace_config.tsx | 203 +++++++++++++ .../components/configuration.tsx | 152 ++++++++++ .../components/create.tsx | 171 +++++++++++ .../components/helpers/modal_containers.tsx | 40 +++ .../components/application_analytics/home.tsx | 148 ++++++++++ .../common/search/autocomplete.test.tsx | 2 +- .../components/common/search/autocomplete.tsx | 2 +- .../common/search/autocomplete_logic.ts | 267 +++++++++++++++++ .../common/search/autocomplete_logic.tsx | 268 ----------------- .../components/common/search/search.test.tsx | 2 +- .../public/components/common/side_nav.tsx | 7 +- .../custom_panels/custom_panel_table.tsx | 9 +- .../components/explorer/event_analytics.tsx | 11 +- .../public/components/explorer/explorer.tsx | 3 +- .../public/components/explorer/home.tsx | 20 +- .../explorer/home_table/saved_query_table.tsx | 9 +- .../components/explorer/log_explorer.tsx | 18 +- .../notebooks/components/note_table.tsx | 8 +- .../common/filters/filter_helpers.tsx | 11 +- .../components/common/filters/filters.tsx | 2 +- .../components/common/search_bar.tsx | 2 +- .../__tests__/dashboard_table.test.tsx | 2 + .../components/dashboard/dashboard.tsx | 46 ++- .../components/dashboard/dashboard_table.tsx | 7 +- .../components/services/services.tsx | 35 ++- .../traces/__tests__/traces.test.tsx | 5 +- .../components/traces/traces.tsx | 35 ++- .../components/trace_analytics/home.tsx | 6 +- .../application_adaptor.ts | 0 .../observability/model/Application.kt | 270 ++++++++++++++++++ .../ObservabilityObjectDataProperties.kt | 5 + .../model/ObservabilityObjectDoc.kt | 5 + .../model/ObservabilityObjectType.kt | 6 + .../opensearch/observability/model/RestTag.kt | 1 + .../main/resources/observability-mapping.yml | 8 + 43 files changed, 2174 insertions(+), 355 deletions(-) create mode 100644 dashboards-observability/common/constants/application_analytics.ts create mode 100644 dashboards-observability/public/components/application_analytics/components/app_table.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/application.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/configuration.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/create.tsx create mode 100644 dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx create mode 100644 dashboards-observability/public/components/application_analytics/home.tsx create mode 100644 dashboards-observability/public/components/common/search/autocomplete_logic.ts delete mode 100644 dashboards-observability/public/components/common/search/autocomplete_logic.tsx create mode 100644 dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts create mode 100644 opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt diff --git a/dashboards-observability/common/constants/application_analytics.ts b/dashboards-observability/common/constants/application_analytics.ts new file mode 100644 index 000000000..59ee7704e --- /dev/null +++ b/dashboards-observability/common/constants/application_analytics.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const TAB_OVERVIEW_ID_TXT_PFX = 'app-analytics-overview-'; +export const TAB_SERVICE_ID_TXT_PFX = 'app-analytics-service-'; +export const TAB_TRACE_ID_TXT_PFX = 'app-analytics-trace-'; +export const TAB_LOG_ID_TXT_PFX = 'app-analytics-log-'; +export const TAB_CONFIG_ID_TXT_PFX = 'app-analytics-config-'; +export const TAB_OVERVIEW_TITLE = 'Overview'; +export const TAB_SERVICE_TITLE = 'Services'; +export const TAB_TRACE_TITLE = 'Traces & Spans'; +export const TAB_LOG_TITLE = 'Log Events'; +export const TAB_CONFIG_TITLE = 'Configuration'; + +export interface optionType { + label: string; +} diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 4838a54ab..26cf80bbf 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import CSS from 'csstype'; + // Client route export const PPL_BASE = '/api/ppl'; export const PPL_SEARCH = '/search'; @@ -58,3 +60,9 @@ export const PLOTLY_COLOR = [ ]; export const LONG_CHART_COLOR = PLOTLY_COLOR[1]; + +export const pageStyles: CSS.Properties = { + float: 'left', + width: '100%', + maxWidth: '1130px', +}; diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index af86b24e8..da4018488 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -14,7 +14,7 @@ import { SELECTED_TIMESTAMP, SELECTED_DATE_RANGE } from '../constants/explorer'; - import { HttpStart, NotificationsStart } from '../../../../src/core/public'; + import { CoreStart, HttpStart, NotificationsStart } from '../../../../src/core/public'; import SavedObjects from '../../public/services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from '../../public/services/timestamp/timestamp'; import PPLService from '../../public/services/requests/ppl'; @@ -98,4 +98,6 @@ export interface IExplorerProps { text?: React.ReactChild | undefined, side?: string | undefined ) => void; -} \ No newline at end of file + http: CoreStart['http']; + tabCreatedTypes?: any; +} diff --git a/dashboards-observability/public/components/app.tsx b/dashboards-observability/public/components/app.tsx index 4c7e57642..38560e08a 100644 --- a/dashboards-observability/public/components/app.tsx +++ b/dashboards-observability/public/components/app.tsx @@ -11,6 +11,7 @@ import { CoreStart } from '../../../../src/core/public'; import { observabilityID, observabilityTitle } from '../../common/constants/shared'; import store from '../framework/redux/store'; import { AppPluginStartDependencies } from '../types'; +import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; import { Home as CustomPanelsHome } from './custom_panels/home'; import { EventAnalytics } from './explorer/event_analytics'; import { Main as NotebooksHome } from './notebooks/components/main'; @@ -50,6 +51,24 @@ export const App = ({ <> + { + return ( + + ) + }} + /> ( @@ -108,7 +127,7 @@ export const App = ({ /> ); }} - /> + /> diff --git a/dashboards-observability/public/components/application_analytics/components/app_table.tsx b/dashboards-observability/public/components/application_analytics/components/app_table.tsx new file mode 100644 index 000000000..254f183ac --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/app_table.tsx @@ -0,0 +1,221 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { AppAnalyticsComponentDeps, ApplicationType } from '../home'; +import { pageStyles } from '../../../../common/constants/shared'; + +interface AppTableProps extends AppAnalyticsComponentDeps { + loading: boolean; + applications: Array; + }; + +export function AppTable(props: AppTableProps) { + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const { applications, parentBreadcrumb } = props; + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + } + ]); + }) + + const popoverButton = ( + setIsActionsPopoverOpen(!isActionsPopoverOpen)} + > + Actions + + ); + + const popoverItems: ReactElement[] = [ + + Rename + , + + Duplicate + , + + Delete + , + + Add sample application + , + ]; + + const tableColumns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + render: (value, record) => ( + {_.truncate(value, { length: 100 })} + ), + }, + { + field: 'composition', + name: 'Composition', + sortable: true, + truncateText: true, + }, + { + field: 'currentAvailability', + name: 'Current Availability', + sortable: true, + truncateText: true, + }, + { + field: 'availabilityMetrics', + name: 'Availability Metrics', + sortable: true, + truncateText: true, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + }> + >; + + return ( +
+ + + + + +

Overview

+
+
+
+ + + + +

+ Applications ({applications.length}) +

+
+
+ + + + setIsActionsPopoverOpen(false)} + > + + + + + + Create application + + + + +
+ + {applications.length > 0 ? ( + + ) : ( + <> + + +

No applications

+
+ + + + + Create application + + + + + Add sample applications + + + + + + )} +
+
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/application.tsx b/dashboards-observability/public/components/application_analytics/components/application.tsx new file mode 100644 index 000000000..3e1816570 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/application.tsx @@ -0,0 +1,244 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, + EuiText, + EuiTitle, + } from '@elastic/eui'; +import { LogExplorer } from '../../explorer/log_explorer'; +import { Dashboard } from '../../trace_analytics/components/dashboard'; +import { Services } from '../../trace_analytics/components/services'; +import { Traces } from '../../trace_analytics/components/traces'; +import { SpanDetailPanel } from '../../trace_analytics/components/traces/span_detail_panel'; +import { Configuration } from './configuration'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import React, { ReactChild, useMemo, useState } from 'react'; +import { isEmpty, uniqueId } from 'lodash'; +import { + TAB_CONFIG_ID_TXT_PFX, + TAB_CONFIG_TITLE, + TAB_LOG_ID_TXT_PFX, + TAB_LOG_TITLE, + TAB_OVERVIEW_ID_TXT_PFX, + TAB_OVERVIEW_TITLE, + TAB_SERVICE_ID_TXT_PFX, + TAB_SERVICE_TITLE, + TAB_TRACE_ID_TXT_PFX, + TAB_TRACE_TITLE +} from '../../../../common/constants/application_analytics'; +import { EmptyTabParams, IQueryTab } from '../../../../common/types/explorer'; +import { useHistory } from 'react-router-dom'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { RAW_QUERY } from '../../../../common/constants/explorer'; +import { NotificationsStart } from '../../../../../../src/core/public'; +import { AppAnalyticsComponentDeps } from '../home'; + + +const TAB_OVERVIEW_ID = uniqueId(TAB_OVERVIEW_ID_TXT_PFX); +const TAB_SERVICE_ID = uniqueId(TAB_SERVICE_ID_TXT_PFX); +const TAB_TRACE_ID = uniqueId(TAB_TRACE_ID_TXT_PFX); +const TAB_LOG_ID = uniqueId(TAB_LOG_ID_TXT_PFX); +const TAB_CONFIG_ID = uniqueId(TAB_CONFIG_ID_TXT_PFX); + +export interface DetailTab { + id: string; + label: string; + description: string; + onClick: () => void; + testId: string; +} + +interface AppDetailProps extends AppAnalyticsComponentDeps { + disabled?: boolean; + appId: string; + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + + +export function Application(props: AppDetailProps) { + const { pplService, dslService, timestampUtils, savedObjects, http, notifications } = props; + const [selectedTabId, setSelectedTab] = useState(TAB_OVERVIEW_ID); + const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedTab(selectedTab.id); + const history = useHistory(); + const [toasts, setToasts] = useState>([]); + + const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { + if (!text) text = ''; + setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); + }; + + const getExistingEmptyTab = ({tabIds, queries, explorerData}: EmptyTabParams) => { + let emptyTabId = ''; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; + if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { + emptyTabId = tid; + break; + } + } + return emptyTabId; + }; + + + const getOverview = () => { + return ( + + ); + }; + + const getService = () => { + return ( + + ); + }; + + const getTrace = () => { + return ( + <> + + + + + ); + }; + + const getLog = () => { + return ( + + ); + }; + + const getConfig = () => { + return ( + + ); + }; + + function getAppAnalyticsTab ({ + tabId, + tabTitle, + getContent + }: { + tabId: string, + tabTitle: string, + getContent: () => JSX.Element + }) { + return { + id: tabId, + name: (<> + + { tabTitle } + + ), + content: ( + <> + { getContent() } + ) + }; + }; + + const getAppAnalyticsTabs = () => { + return [ + getAppAnalyticsTab( + { + tabId: TAB_OVERVIEW_ID, + tabTitle: TAB_OVERVIEW_TITLE, + getContent: () => getOverview() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_SERVICE_ID, + tabTitle: TAB_SERVICE_TITLE, + getContent: () => getService() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_TRACE_ID, + tabTitle: TAB_TRACE_TITLE, + getContent: () => getTrace() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_LOG_ID, + tabTitle: TAB_LOG_TITLE, + getContent: () => getLog() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_CONFIG_ID, + tabTitle: TAB_CONFIG_TITLE, + getContent: () => getConfig() + } + ) + ]; + }; + + + const memorizedAppAnalyticsTabs = useMemo(() => { + return getAppAnalyticsTabs(); + }, + []); + + return ( +
+ + + + + +

my-app1

+
+
+
+ { tab.id === selectedTabId }) } + onTabClick={ (selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab) } + tabs={ memorizedAppAnalyticsTabs } + /> +
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx new file mode 100644 index 000000000..297e409c4 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiText, EuiSpacer, EuiButton, EuiFormRow, EuiFlexItem, EuiBadge, EuiOverlayMask } from "@elastic/eui"; +import { uiSettingsService } from "../../../../../common/utils"; +import { Autocomplete } from "../../../common/search/autocomplete"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import{ getClearModal } from "../helpers/modal_containers"; + +interface LogConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + setIsFlyoutVisible: any; +} + +export const LogConfig = (props: LogConfigProps) => { + const { dslService, query, setQuery, setIsFlyoutVisible } = props; + const [logOpen, setLogOpen] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); + const tempQuery =''; + + const handleQueryChange = async (query: string) => setQuery(query); + + const showFlyout = () => { + setIsFlyoutVisible(true); + }; + + const onCancel = () => { + setIsModalVisible(false); + } + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onConfirm = () => { + handleQueryChange(''); + closeModal(); + } + + const clearAllModal = () => { + setModalLayout( + getClearModal( + onCancel, + onConfirm, + 'Clear log source', + 'Are you sure you would like to clear the log source?', + 'Clear' + ) + ); + showModal(); + }; + + return ( +
+ + +

Log Source

+
+ + + Configure your application base query + + + } + extraAction={Clear} + onToggle={(isOpen) => {setLogOpen(isOpen)}} + paddingSize="l" + > + + + {}} + dslService={dslService} + /> + showFlyout()} + onClickAriaLabel={"pplLinkShowFlyout"} + > + PPL + + + +
+ {isModalVisible && modalLayout} +
+ ); +} \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx new file mode 100644 index 000000000..9afed9b4c --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { FilterType } from "../../../trace_analytics/components/common/filters/filters"; +import { ServiceObject } from "../../../trace_analytics/components/common/plots/service_map"; +import { ServiceMap } from "../../../trace_analytics/components/services"; +import { handleServiceMapRequest } from "../../../trace_analytics/requests/services_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { useEffect } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { optionType } from "common/constants/application_analytics"; + +interface ServiceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedServices: Array; + setSelectedServices: (services: Array) => void; +} + +export const ServiceConfig = (props: ServiceConfigProps) => { + const { dslService, filters, setFilters, http, selectedServices, setSelectedServices } = props; + const [servicesOpen, setServicesOpen] = useState(false); + const [serviceMap, setServiceMap] = useState({}); + const [serviceMapIdSelected, setServiceMapIdSelected] = useState<'latency' | 'error_rate' | 'throughput'>('latency'); + + useEffect(() => { + handleServiceMapRequest(http, dslService, serviceMap, setServiceMap); + }, []) + + useEffect (() => { + const serviceOptions = filters.filter(f => f.field === 'serviceName').map((f) => { return { label: f.value }}); + const noDups = serviceOptions.filter((s, index) => { return serviceOptions.findIndex(ser => ser.label === s.label) === index }); + setSelectedServices(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onServiceChange = (selectedServices: any) => { + const serviceFilters = selectedServices.map((option: optionType) => { + return { + field: 'serviceName', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonServiceFilters = filters.filter((f) => f.field !== 'serviceName'); + setFilters([...nonServiceFilters, ...serviceFilters]); + }; + + const clearServices = () => { + const withoutServices = filters.filter((f) => f.field !== 'serviceName') + setFilters(withoutServices); + }; + + const services = Object.keys(serviceMap).map((service) => { return { label: service } }); + + return ( + + +

+ Services & Entities {selectedServices.length} +

+
+ + + Select services & entities to include in this application + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setServicesOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx new file mode 100644 index 000000000..e17fa78fd --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx @@ -0,0 +1,203 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dateMath from '@elastic/datemath'; +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { optionType } from "common/constants/application_analytics"; +import { filtersToDsl } from "../../../trace_analytics/components/common/helper_functions"; +import { handleDashboardRequest } from "../../../trace_analytics/requests/dashboard_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { DashboardTable } from '../../../trace_analytics/components/dashboard/dashboard_table'; +import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; + +interface TraceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedTraces: Array; + setSelectedTraces: (traces: Array) => void; +} + +export const TraceConfig = (props: TraceConfigProps) => { + const { dslService, query, filters, setFilters, http, startTime, endTime, selectedTraces, setSelectedTraces } = props; + const [traceOpen, setTraceOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [traceItems, setTraceItems] = useState([]); + const [traceOptions, setTraceOptions] = useState>([]); + const [percentileMap, setPercentileMap] = useState<{ [traceGroup: string]: number[] }>({}); + const [redirect, setRedirect] = useState(true); + + useEffect(() => { + setLoading(true) + const timeFilterDSL = filtersToDsl([], '', startTime, endTime); + const latencyTrendStartTime = dateMath + .parse(endTime) + ?.subtract(24, 'hours') + .toISOString()!; + const latencyTrendDSL = filtersToDsl( + filters, + query, + latencyTrendStartTime, + endTime + ); + handleDashboardRequest( + http, + dslService, + timeFilterDSL, + latencyTrendDSL, + traceItems, + setTraceItems, + setPercentileMap + ).then(() => setLoading(false)); + setRedirect(false); + }, []) + + useEffect (() => { + const toOptions = traceItems.map((item: any) => { return { label: item.dashboard_trace_group_name }}); + setTraceOptions(toOptions); + }, [traceItems]) + + useEffect (() => { + const filteredOptions = filters.filter(f => f.field === 'traceGroup').map((f) => { return { label: f.value }}); + const noDups = filteredOptions.filter((t, index) => { return filteredOptions.findIndex(trace => trace.label === t.label) === index }); + setSelectedTraces(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onTraceChange = (selectedTraces: any) => { + const traceFilters = selectedTraces.map((option: optionType) => { + return { + field: 'traceGroup', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonTraceFilters = filters.filter((f) => f.field !== 'traceGroup'); + setFilters([...nonTraceFilters, ...traceFilters]); + }; + + const onCreateTrace = (searchValue: string, flattenedOptions: any) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + if (!normalizedSearchValue) { + return; + } + const newTraceOption = { + label: searchValue + } + const newTraceFilter = { + field: 'traceGroup', + operator: 'is', + value: searchValue, + inverted: false, + disabled: false + }; + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + (option: optionType) => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + setTraceOptions([...traceOptions, newTraceOption]); + } + // Select the option. + setFilters([...filters, newTraceFilter]); + }; + + const addPercentileFilter = (condition = 'gte', additionalFilters = [] as FilterType[]) => { + if (traceItems.length === 0 || Object.keys(percentileMap).length === 0) return; + for (let i = 0; i < props.filters.length; i++) { + if (props.filters[i].custom) { + const newFilter = JSON.parse(JSON.stringify(props.filters[i])); + newFilter.custom.query.bool.should.forEach((should: any) => + should.bool.must.forEach((must: any) => { + const range = must?.range?.['traceGroupFields.durationInNanos']; + if (range) { + const duration = range.lt || range.lte || range.gt || range.gte; + if (duration || duration === 0) { + must.range['traceGroupFields.durationInNanos'] = { + [condition]: duration, + }; + } + } + }) + ); + newFilter.value = condition === 'gte' ? '>= 95th' : '< 95th'; + const newFilters = [...filters, ...additionalFilters]; + newFilters.splice(i, 1, newFilter); + setFilters(newFilters); + return; + } + } + } + + const clearTraces = () => { + const withoutTraces = filters.filter((f) => f.field !== 'traceGroup') + setFilters(withoutTraces); + }; + + return ( + + +

+ Trace Groups {selectedTraces.length} +

+
+ + + Constrain your application to specific trace groups + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setTraceOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/configuration.tsx b/dashboards-observability/public/components/application_analytics/components/configuration.tsx new file mode 100644 index 000000000..0cf800892 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/configuration.tsx @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle +} from '@elastic/eui'; +import React from 'react'; + +const dummy = [{ + level: "Available", + definition: "error rate below or equal to 1%", + id: "1" +}]; + +const dummyLogSources = [ + {logName: "index_1"}, {logName: "ingest_logs_all"} +]; + +const dummyServicesEntities = [ + {serviceName: "Payment"}, {serviceName: "Users"}, {serviceName: "Purchase"} +]; + +const dummyTraceGroups = [ + {traceGroup: "Payment.auto"}, {traceGroup: "Users.admin"}, {traceGroup: "Purchase.source"} +]; + +export const Configuration = () => { + + const tableColumns = [ + { + field: 'level', + name: 'Level', + render: (value) => value, + }, + { + field: 'definition', + name: 'Definition', + render: (value) => value, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + level: string; + id: string; + definition: string; + }> + >; + + return ( +
+ + + + + + +

+ Composition +

+
+
+ + + + {}}> + Edit composition + + + + +
+ + + + +
Log Sources
+
    + {dummyLogSources.map(function(item, index){ + return
  • {item.logName}
  • + })} +
+
+
+ + +
Services & Entities
+
    + {dummyServicesEntities.map(function(item, index){ + return
  • {item.serviceName}
  • + })} +
+
+
+ + +
Trace groups
+
    + {dummyTraceGroups.map(function(item, index){ + return
  • {item.traceGroup}
  • + })} +
+
+
+
+
+
+
+ + + + + + +

+ Availability +

+
+
+ + + + {}}> + Edit availability + + + + +
+ + +
+
+
+
+ ) +} diff --git a/dashboards-observability/public/components/application_analytics/components/create.tsx b/dashboards-observability/public/components/application_analytics/components/create.tsx new file mode 100644 index 000000000..f71389778 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/create.tsx @@ -0,0 +1,171 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTitle, + EuiToolTip +} from "@elastic/eui"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { ChangeEvent } from "react"; +import { AppAnalyticsComponentDeps } from "../home"; +import { TraceConfig } from './config_components/trace_config'; +import { ServiceConfig } from "./config_components/service_config"; +import { LogConfig } from "./config_components/log_config"; +import { PPLReferenceFlyout } from "../../../components/common/helpers"; +import { optionType } from "common/constants/application_analytics"; + +interface CreateAppProps extends AppAnalyticsComponentDeps { + dslService: DSLService; +}; + +export const CreateApp = (props: CreateAppProps) => { + const { parentBreadcrumb, chrome, query } = props; + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [selectedServices, setSelectedServices] = useState>([]); + const [selectedTraces, setSelectedTraces] = useState>([]); + const [state, setState] = useState({ + name: '', + description: '' + }); + + useEffect(() => { + chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: 'Create', + href: '#/application_analytics/create', + }, + ]); + }, []) + + const closeFlyout = () => { + setIsFlyoutVisible(false); + }; + + let flyout; + if (isFlyoutVisible) { + flyout = ; + } + + const onChange = (e: ChangeEvent) => { + setState({ + ...state, + [e.target.name]: e.target.value + }); + }; + + const isDisabled = !state.name || !query || !selectedTraces.length || !selectedServices.length; + + const missingField = () => { + if (isDisabled) { + let popoverContent = ''; + if (!state.name) { + popoverContent = 'Name is required.' + } else if (!query) { + popoverContent = 'Log Source is required.' + } else if (!selectedServices.length) { + popoverContent = 'Services & Entities is required.' + } else if (!selectedTraces.length) { + popoverContent = 'Trace Groups are required.' + } + return

{popoverContent}

; + } + }; + + return ( +
+ + + + + +

Create application

+
+
+
+ + + + +

Application information

+
+
+
+ + + + onChange(e)} + /> + + + onChange(e)} + /> + + +
+ + + + + +

Composition

+
+
+
+ + + + + + +
+ + + + + Cancel + + + + + + Create + + + + +
+
+ {flyout} +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx new file mode 100644 index 000000000..c9d54f9c2 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; + +/* The file contains helper functions for modal layouts + * getDeleteModal - returns a confirm-modal with clear option + */ + +export const getClearModal = ( + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void, + onConfirm: (event?: React.MouseEvent) => void, + title: string, + message: string, + confirmMessage?: string +) => { + return ( + + + {message} + + + ); +}; \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/home.tsx b/dashboards-observability/public/components/application_analytics/home.tsx new file mode 100644 index 000000000..85d7d8466 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/home.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +import React, { useEffect, useState } from 'react'; +import { AppTable } from './components/app_table'; +import { Application } from './components/application'; +import { CreateApp } from './components/create' +import { Route, RouteComponentProps, Switch } from 'react-router'; +import { TraceAnalyticsComponentDeps, TraceAnalyticsCoreDeps } from '../trace_analytics/home'; +import { FilterType } from '../trace_analytics/components/common/filters/filters'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import { handleIndicesExistRequest } from '../trace_analytics/requests/request_handler'; +import { ObservabilitySideBar } from '../common/side_nav'; +import { NotificationsStart } from '../../../../../src/core/public'; + +export interface AppAnalyticsCoreDeps extends TraceAnalyticsCoreDeps {} + +interface HomeProps extends RouteComponentProps, AppAnalyticsCoreDeps { + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + +export interface AppAnalyticsComponentDeps extends TraceAnalyticsComponentDeps {} + +export type ApplicationType = { + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + dateCreated: string; + dateModified: string; +}; + +const dateString = new Date().toISOString(); + +const dummyApplication: ApplicationType[] = [{ + name: "Cool Application", + id: "id", + composition: "Payment, user_db", + currentAvailability: "Available", + availabilityMetrics: "Error rate: 0.80%, Throughput: 0.94%, Latency: 600ms", + dateCreated: dateString, + dateModified: dateString +}]; + +export const Home = (props: HomeProps) => { + const { pplService, dslService, timestampUtils, savedObjects, parentBreadcrumb, http, chrome, notifications } = props; + const [indicesExist, setIndicesExist] = useState(true); + const storedFilters = sessionStorage.getItem('AppAnalyticsFilters'); + const [query, setQuery] = useState(sessionStorage.getItem('AppAnalyticsQuery') || ''); + const [filters, setFilters] = useState( + storedFilters ? JSON.parse(storedFilters) : [] + ); + const [startTime, setStartTime] = useState( + sessionStorage.getItem('AppAnalyticsStartTime') || 'now-24h' + ); + const [endTime, setEndTime] = useState( + sessionStorage.getItem('AppAnalyticsEndTime') || 'now' + ); + + const setFiltersWithStorage = (newFilters: FilterType[]) => { + setFilters(newFilters); + sessionStorage.setItem('AppAnalyticsFilters', JSON.stringify(newFilters)); + }; + const setQueryWithStorage = (newQuery: string) => { + setQuery(newQuery); + sessionStorage.setItem('AppAnalyticsQuery', newQuery); + }; + const setStartTimeWithStorage = (newStartTime: string) => { + setStartTime(newStartTime); + sessionStorage.setItem('AppAnalyticsStartTime', newStartTime); + }; + const setEndTimeWithStorage = (newEndTime: string) => { + setEndTime(newEndTime); + sessionStorage.setItem('AppAnalyticsEndTime', newEndTime); + }; + + useEffect(() => { + handleIndicesExistRequest(http, setIndicesExist); + }, []); + + const commonProps: AppAnalyticsComponentDeps = { + parentBreadcrumb: parentBreadcrumb, + http: http, + chrome: chrome, + query, + setQuery: setQueryWithStorage, + filters, + setFilters: setFiltersWithStorage, + startTime, + setStartTime: setStartTimeWithStorage, + endTime, + setEndTime: setEndTimeWithStorage, + indicesExist, + }; + + return ( +
+ + + + + + } + /> + + + } + /> + + + } + /> + +
+ ) +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete.test.tsx b/dashboards-observability/public/components/common/search/autocomplete.test.tsx index e5b740d07..94b3ecdfb 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.test.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.test.tsx @@ -37,7 +37,7 @@ describe('renders autocomplete', function () { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); it('handles query change', () => { act(() => { diff --git a/dashboards-observability/public/components/common/search/autocomplete.tsx b/dashboards-observability/public/components/common/search/autocomplete.tsx index a42b47ce7..410bcd9c6 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.tsx @@ -106,7 +106,7 @@ export const Autocomplete = (props: IQueryBarProps) => { {...autocomplete.getInputProps({ id: 'autocomplete-textarea', "data-test-subj": "searchAutocompleteTextArea", - placeholder: 'Enter PPL query to retrieve logs', + placeholder: 'Enter PPL query', inputElement: null })} /> diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.ts b/dashboards-observability/public/components/common/search/autocomplete_logic.ts new file mode 100644 index 000000000..09b1c58a5 --- /dev/null +++ b/dashboards-observability/public/components/common/search/autocomplete_logic.ts @@ -0,0 +1,267 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getDataValueQuery } from './queries/data_queries'; +import DSLService from 'public/services/requests/dsl'; +import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; + +let currIndex: string = ''; +let currField: string = ''; +let currFieldType: string = ''; + +let inFieldsCommaLoop: boolean = false; +let inMatch: boolean = false; +let nextWhere: number = Number.MAX_SAFE_INTEGER; +let nextStats: number = Number.MAX_SAFE_INTEGER; + +const indexList: string[] = []; +const fieldList: string[] = []; +const fieldsFromBackend: fieldItem[] = []; +const indicesFromBackend: indexItem[] = []; +const dataValuesFromBackend: dataItem[] = []; + +const getIndices = async (dslService: DSLService): Promise => { + if (indicesFromBackend.length === 0) { + const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); + for (let i = 0; i < indices.length; i++) { + indicesFromBackend.push({ + label: indices[i].index, + }); + indexList.push(indices[i].index); + } + } +}; + +const getFields = async (dslService: DSLService): Promise => { + if (currIndex !== '') { + const res = await dslService.fetchFields(currIndex); + fieldsFromBackend.length = 0; + for (const element in res?.[currIndex].mappings.properties) { + if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else { + fieldsFromBackend.push({ + label: element, + type: res?.[currIndex].mappings.properties[element].type, + }); + } + fieldList.push(element); + } + } +}; + +const getDataValues = async ( + index: string, + field: string, + fieldType: string, + dslService: DSLService +): Promise => { + const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; + dataValuesFromBackend.length = 0; + res.forEach((e: any) => { + if (fieldType === 'string') { + dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); + } else if (fieldType === 'boolean') { + if (e.key === 1) { + dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); + } else { + dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); + } + } else if (fieldType !== 'geo_point') { + dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); + } + }); + return dataValuesFromBackend; +}; + +export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { + if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { + currIndex = item.itemName; + getFields(dslService); + } + setQuery(item.label + ' '); +}; + +// Function to create the array of objects to be suggested +const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { + const lowerWord = word.toLowerCase(); + const filteredList = items.filter( + (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) + ); + const suggestionList = []; + for (let i = 0; i < filteredList.length; i++) { + suggestionList.push({ + label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, + input: str, + suggestion: filteredList[i].label.substring(word.length), + itemName: filteredList[i].label, + }); + } + return suggestionList; +}; + +// Function for the first command in query, also needs to get available indices +const getFirstPipe = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + getIndices(dslService); + return fillSuggestions(str, prefix, firstCommand); +}; + +// Main logic behind autocomplete (Based on most recent inputs) +export const getSuggestions = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + const lowerPrefix = prefix.toLowerCase(); + const fullSuggestions: AutocompleteItem[] = []; + + // Check the last full word in the query, then suggest inputs based off that + if (splittedModel.length === 1) { + currField = ''; + currIndex = ''; + return getFirstPipe(str, dslService); + } else if (splittedModel.length > 1) { + if (splittedModel[splittedModel.length - 2] === '|') { + inFieldsCommaLoop = false; + inMatch = false; + nextWhere = Number.MAX_SAFE_INTEGER; + nextStats = Number.MAX_SAFE_INTEGER; + currField = ''; + currFieldType = ''; + return fillSuggestions(str, prefix, pipeCommands); + } else if (splittedModel[splittedModel.length - 2].includes(',')) { + if (inFieldsCommaLoop) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + if (inMatch) { + inMatch = true; + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } + return fullSuggestions; + } else if ( + splittedModel[splittedModel.length - 2] === 'source' + ) { + return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if ( + (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') + ) { + return fillSuggestions(str, prefix, indicesFromBackend); + } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { + currIndex = splittedModel[splittedModel.length - 2]; + getFields(dslService); + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (splittedModel[splittedModel.length - 2] === 'stats') { + nextStats = splittedModel.length; + return fillSuggestions(str, prefix, statsCommands); + } else if (nextStats === splittedModel.length - 1) { + if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { + if (splittedModel[splittedModel.length - 2] === 'count()') { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + const numberFields = fieldsFromBackend.filter( + (field: { label: string, type: string }) => + field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) + ); + for (let i = 0; i < numberFields.length; i++) { + var field: {label: string} = numberFields[i]; + fullSuggestions.push({ + label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', + input: str, + suggestion: field.label.substring(prefix.length) + ' )', + itemName: field.label + ' )', + }); + } + return fullSuggestions; + } + } + } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextStats === splittedModel.length - 3) { + if (splittedModel[splittedModel.length - 3] === 'by') { + return [ + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } + } else if (nextStats === splittedModel.length - 4) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + else if (splittedModel[splittedModel.length - 2] === 'fields') { + inFieldsCommaLoop = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'dedup') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'where') { + nextWhere = splittedModel.length; + return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); + } else if (splittedModel[splittedModel.length - 2] === 'match(') { + inMatch = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextWhere === splittedModel.length - 1) { + fullSuggestions.push({ + label: str + '=', + input: str, + suggestion: '=', + itemName: '=', + }); + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); + } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } else if (nextWhere === splittedModel.length - 2) { + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (inFieldsCommaLoop) { + return [ + { + label: str.substring(0, str.length - 1) + ',', + input: str.substring(0, str.length - 1), + suggestion: ',', + itemName: ',', + }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' }, + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); + } else if (inMatch) { + inMatch = false; + return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } + return []; + } +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx b/dashboards-observability/public/components/common/search/autocomplete_logic.tsx deleted file mode 100644 index 375bd77dd..000000000 --- a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getDataValueQuery } from './queries/data_queries'; -import DSLService from 'public/services/requests/dsl'; -import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; - -let currIndex: string = ''; -let currField: string = ''; -let currFieldType: string = ''; - -let inFieldsCommaLoop: boolean = false; -let inMatch: boolean = false; -let nextWhere: number = Number.MAX_SAFE_INTEGER; -let nextStats: number = Number.MAX_SAFE_INTEGER; - -const indexList: string[] = []; -const fieldList: string[] = []; -const fieldsFromBackend: fieldItem[] = []; -const indicesFromBackend: indexItem[] = []; -const dataValuesFromBackend: dataItem[] = []; - -const getIndices = async (dslService: DSLService): Promise => { - if (indicesFromBackend.length === 0) { - const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); - for (let i = 0; i < indices.length; i++) { - indicesFromBackend.push({ - label: indices[i].index, - }); - indexList.push(indices[i].index); - } - } - }; - - const getFields = async (dslService: DSLService): Promise => { - if (currIndex !== '') { - const res = await dslService.fetchFields(currIndex); - fieldsFromBackend.length = 0; - for (const element in res?.[currIndex].mappings.properties) { - if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else { - fieldsFromBackend.push({ - label: element, - type: res?.[currIndex].mappings.properties[element].type, - }); - } - fieldList.push(element); - } - } - }; - - const getDataValues = async ( - index: string, - field: string, - fieldType: string, - dslService: DSLService - ): Promise => { - const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; - dataValuesFromBackend.length = 0; - res.forEach((e: any) => { - if (fieldType === 'string') { - dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); - } else if (fieldType === 'boolean') { - if (e.key === 1) { - dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); - } else { - dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); - } - } else if (fieldType !== 'geo_point') { - dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); - } - }); - return dataValuesFromBackend; - }; - -export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { - if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { - currIndex = item.itemName; - getFields(dslService); - } - setQuery(item.label + ' '); - }; - -// Function to create the array of objects to be suggested - const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { - const lowerWord = word.toLowerCase(); - const filteredList = items.filter( - (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) - ); - const suggestionList = []; - for (let i = 0; i < filteredList.length; i++) { - suggestionList.push({ - label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, - input: str, - suggestion: filteredList[i].label.substring(word.length), - itemName: filteredList[i].label, - }); - } - return suggestionList; - }; - - // Function for the first command in query, also needs to get available indices - const getFirstPipe = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - getIndices(dslService); - return fillSuggestions(str, prefix, firstCommand); - }; - - // Main logic behind autocomplete (Based on most recent inputs) - export const getSuggestions = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - const lowerPrefix = prefix.toLowerCase(); - const fullSuggestions: AutocompleteItem[] = []; - - // Check the last full word in the query, then suggest inputs based off that - if (splittedModel.length === 1) { - currField = ''; - currIndex = ''; - return getFirstPipe(str, dslService); - } else if (splittedModel.length > 1) { - if (splittedModel[splittedModel.length - 2] === '|') { - inFieldsCommaLoop = false; - inMatch = false; - nextWhere = Number.MAX_SAFE_INTEGER; - nextStats = Number.MAX_SAFE_INTEGER; - currField = ''; - currFieldType = ''; - return fillSuggestions(str, prefix, pipeCommands); - } else if (splittedModel[splittedModel.length - 2].includes(',')) { - if (inFieldsCommaLoop) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - if (inMatch) { - inMatch = true; - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } - return fullSuggestions; - } else if ( - splittedModel[splittedModel.length - 2] === 'source' - ) { - return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if ( - (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') - ) { - return fillSuggestions(str, prefix, indicesFromBackend); - } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { - currIndex = splittedModel[splittedModel.length - 2]; - getFields(dslService); - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (splittedModel[splittedModel.length - 2] === 'stats') { - nextStats = splittedModel.length; - return fillSuggestions(str, prefix, statsCommands); - } else if (nextStats === splittedModel.length - 1) { - if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { - if (splittedModel[splittedModel.length - 2] === 'count()') { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - const numberFields = fieldsFromBackend.filter( - (field: { label: string, type: string }) => - field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) - ); - for (let i = 0; i < numberFields.length; i++) { - var field: {label: string} = numberFields[i]; - fullSuggestions.push({ - label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', - input: str, - suggestion: field.label.substring(prefix.length) + ' )', - itemName: field.label + ' )', - }); - } - return fullSuggestions; - } - } - } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextStats === splittedModel.length - 3) { - if (splittedModel[splittedModel.length - 3] === 'by') { - return [ - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } - } else if (nextStats === splittedModel.length - 4) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - else if (splittedModel[splittedModel.length - 2] === 'fields') { - inFieldsCommaLoop = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'dedup') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'where') { - nextWhere = splittedModel.length; - return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); - } else if (splittedModel[splittedModel.length - 2] === 'match(') { - inMatch = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextWhere === splittedModel.length - 1) { - fullSuggestions.push({ - label: str + '=', - input: str, - suggestion: '=', - itemName: '=', - }); - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); - } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } else if (nextWhere === splittedModel.length - 2) { - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (inFieldsCommaLoop) { - return [ - { - label: str.substring(0, str.length - 1) + ',', - input: str.substring(0, str.length - 1), - suggestion: ',', - itemName: ',', - }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' }, - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); - } else if (inMatch) { - inMatch = false; - return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } - return []; - } - }; - \ No newline at end of file diff --git a/dashboards-observability/public/components/common/search/search.test.tsx b/dashboards-observability/public/components/common/search/search.test.tsx index 37c934386..74dcd1a0a 100644 --- a/dashboards-observability/public/components/common/search/search.test.tsx +++ b/dashboards-observability/public/components/common/search/search.test.tsx @@ -48,7 +48,7 @@ describe('Search bar', () => { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); fireEvent.change(searchBar, { target: { value: 'new query' } }); expect(handleQueryChange).toBeCalledWith('new query'); }); diff --git a/dashboards-observability/public/components/common/side_nav.tsx b/dashboards-observability/public/components/common/side_nav.tsx index 83129f315..c00a3e763 100644 --- a/dashboards-observability/public/components/common/side_nav.tsx +++ b/dashboards-observability/public/components/common/side_nav.tsx @@ -32,7 +32,7 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { // Default page is Events Analytics // But it is kept as second option in side nav if (hash === '#/') { - items[0].items[1].isSelected = true; + items[0].items[2].isSelected = true; return true; } for (let i = 0; i < items.length; i++) { @@ -51,6 +51,11 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { name: 'Observability', id: 0, items: [ + { + name: 'Application analytics', + id: 1, + href: '#/application_analytics', + }, { name: 'Trace analytics', id: 1, diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 3b4d2d569..185fc3beb 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -28,7 +28,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { CSSProperties, ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE, @@ -40,12 +40,7 @@ import moment from 'moment'; import _ from 'lodash'; import { CustomPanelListType } from '../../../common/types/custom_panels'; import { getSampleDataModal } from '../common/helpers/add_sample_modal'; - -const pageStyles: CSSProperties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../common/constants/shared'; /* * "CustomPanelTable" module, used to view all the saved panels diff --git a/dashboards-observability/public/components/explorer/event_analytics.tsx b/dashboards-observability/public/components/explorer/event_analytics.tsx index 248bdcacd..2b90c152d 100644 --- a/dashboards-observability/public/components/explorer/event_analytics.tsx +++ b/dashboards-observability/public/components/explorer/event_analytics.tsx @@ -5,6 +5,7 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { EmptyTabParams } from 'common/types/explorer'; import { isEmpty } from 'lodash'; import React, { ReactChild, useState } from 'react'; import { HashRouter, Route, Switch, useHistory } from 'react-router-dom'; @@ -37,10 +38,10 @@ export const EventAnalytics = ({ setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); }; - const getExistingEmptyTab = ({ tabIds, queries, explorerData }) => { + const getExistingEmptyTab = ({ tabIds, queries, explorerData }: EmptyTabParams) => { let emptyTabId = ''; - for (let i = 0; i < tabIds.length; i++) { - const tid = tabIds[i]; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { emptyTabId = tid; break; @@ -80,7 +81,6 @@ export const EventAnalytics = ({ timestampUtils={timestampUtils} http={http} setToast={setToast} - chrome={chrome} getExistingEmptyTab={getExistingEmptyTab} history={history} notifications={notifications} @@ -106,10 +106,9 @@ export const EventAnalytics = ({ http={http} savedObjects={savedObjects} dslService={dslService} - timestampUtils={timestampUtils} + pplService={pplService} setToast={setToast} getExistingEmptyTab={getExistingEmptyTab} - history={history} /> ); diff --git a/dashboards-observability/public/components/explorer/explorer.tsx b/dashboards-observability/public/components/explorer/explorer.tsx index 3aa13d7e0..2eabe2637 100644 --- a/dashboards-observability/public/components/explorer/explorer.tsx +++ b/dashboards-observability/public/components/explorer/explorer.tsx @@ -72,7 +72,8 @@ export const Explorer = ({ setToast, history, notifications, - savedObjectId + savedObjectId, + tabCreatedTypes }: IExplorerProps) => { const dispatch = useDispatch(); const requestParams = { tabId, }; diff --git a/dashboards-observability/public/components/explorer/home.tsx b/dashboards-observability/public/components/explorer/home.tsx index d2e1ac5b4..c7b15d287 100644 --- a/dashboards-observability/public/components/explorer/home.tsx +++ b/dashboards-observability/public/components/explorer/home.tsx @@ -86,8 +86,8 @@ export const Home = (props: IHomeProps) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedDateRange, setSelectedDateRange] = useState>(['now-15m', 'now']); - const [savedHistories, setSavedHistories] = useState([]); - const [selectedHisotries, setSelectedHisotries] = useState([]); + const [savedHistories, setSavedHistories] = useState>([]); + const [selectedHistories, setSelectedHistories] = useState>([]); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isTableLoading, setIsTableLoading] = useState(false); const [modalLayout, setModalLayout] = useState(); @@ -113,7 +113,7 @@ export const Home = (props: IHomeProps) => { }; const deleteHistoryList = async () => { - const objectIdsToDelete = selectedHisotries.map((history) => history.data.objectId); + const objectIdsToDelete = selectedHistories.map((history) => history.data.objectId); await savedObjects .deleteSavedObjectsList({ objectIdList: objectIdsToDelete }) .then(async (res) => { @@ -264,7 +264,7 @@ export const Home = (props: IHomeProps) => { }); }); setToast(`Sample events added successfully.`); - } catch (error) { + } catch (error: any) { setToast(`Cannot add sample events data, error: ${error}`, 'danger'); console.error(error.body.message); } finally { @@ -284,13 +284,13 @@ export const Home = (props: IHomeProps) => { ); const deleteHistory = () => { - const customPanelString = `${selectedHisotries.length > 1 ? 'histories' : 'history'}`; + const customPanelString = `${selectedHistories.length > 1 ? 'histories' : 'history'}`; setModalLayout( ); showModal(); @@ -299,7 +299,7 @@ export const Home = (props: IHomeProps) => { const popoverItems: ReactElement[] = [ { setIsActionsPopoverOpen(false); deleteHistory(); @@ -408,8 +408,8 @@ export const Home = (props: IHomeProps) => { savedHistories={savedHistories} handleHistoryClick={handleHistoryClick} isTableLoading={isTableLoading} - handleSelectHistory={setSelectedHisotries} - selectedHisotries={selectedHisotries} + handleSelectHistory={setSelectedHistories} + selectedHistories={selectedHistories} /> ) : ( <> diff --git a/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx index 50802c13b..0c3abcad3 100644 --- a/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx +++ b/dashboards-observability/public/components/explorer/home_table/saved_query_table.tsx @@ -16,6 +16,7 @@ interface savedQueryTableProps { handleHistoryClick: (objectId: string) => void; handleSelectHistory: (selectedHistories: Array) => void; isTableLoading: boolean; + selectedHistories: Array; } export function SavedQueryTable({ @@ -26,9 +27,9 @@ export function SavedQueryTable({ }: savedQueryTableProps) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); - const pageIndexRef = useRef(); + const pageIndexRef = useRef(); pageIndexRef.current = pageIndex; - const pageSizeRef = useRef(); + const pageSizeRef = useRef(); pageSizeRef.current = pageSize; const onTableChange = ({ page = {} }) => { @@ -44,7 +45,7 @@ export function SavedQueryTable({ name: '', sortable: true, width: '40px', - render: (item) => { + render: (item: any) => { if (item == 'Visualization') { return (
@@ -66,7 +67,7 @@ export function SavedQueryTable({ width: '70%', sortable: true, truncateText: true, - render: (item) => { + render: (item: any) => { return ( { diff --git a/dashboards-observability/public/components/explorer/log_explorer.tsx b/dashboards-observability/public/components/explorer/log_explorer.tsx index 91a9578c1..4d8cf3a07 100644 --- a/dashboards-observability/public/components/explorer/log_explorer.tsx +++ b/dashboards-observability/public/components/explorer/log_explorer.tsx @@ -36,6 +36,7 @@ export const LogExplorer = ({ getExistingEmptyTab, history, notifications, + http }: ILogExplorerProps) => { const dispatch = useDispatch(); @@ -178,17 +179,18 @@ export const LogExplorer = ({ <> + tabCreatedTypes={tabCreatedTypes} + http={http} + /> ) }; } diff --git a/dashboards-observability/public/components/notebooks/components/note_table.tsx b/dashboards-observability/public/components/notebooks/components/note_table.tsx index 8d38bcbe4..c3810c11c 100644 --- a/dashboards-observability/public/components/notebooks/components/note_table.tsx +++ b/dashboards-observability/public/components/notebooks/components/note_table.tsx @@ -27,7 +27,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import CSS from 'csstype'; import _ from 'lodash'; import moment from 'moment'; import React, { ReactElement, useEffect, useState } from 'react'; @@ -43,12 +42,7 @@ import { getSampleNotebooksModal, } from './helpers/modal_containers'; import { NotebookType } from './main'; - -const pageStyles: CSS.Properties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../../common/constants/shared'; type NoteTableProps = { loading: boolean; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx index 15eaf5a33..1410bfea0 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx @@ -13,22 +13,23 @@ import { import _ from 'lodash'; import React from 'react'; -const getFields = (page: 'dashboard' | 'traces' | 'services') => +const getFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => ({ dashboard: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], traces: ['traceId', 'traceGroup', 'serviceName', 'error', 'status.message', 'latency'], services: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], + app: ['traceId', 'traceGroup', 'serviceName'], }[page]); // filters will take effect and can be manually added -export const getFilterFields = (page: 'dashboard' | 'traces' | 'services') => getFields(page); +export const getFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => getFields(page); // filters will take effect -export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services') => { +export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => { const fields = getFields(page); if (page !== 'services') return [...fields, 'Latency percentile within trace group']; return fields; }; -const getType = (field: string): string => { +const getType = (field: string): string | null => { const typeMapping = { attributes: { host: { @@ -106,7 +107,7 @@ export const getOperatorOptions = (field: string) => { }; const operators = [ ...operatorMapping.default_first, - ...operatorMapping[type], + ..._.get(operatorMapping, type), ...operatorMapping.default_last, ]; return operators; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx index 7fdf9fed2..af3870626 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx @@ -36,7 +36,7 @@ export interface FiltersProps { } interface FiltersOwnProps extends FiltersProps { - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; } export function Filters(props: FiltersOwnProps) { diff --git a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx index ec876522e..d1dfe5a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx @@ -47,7 +47,7 @@ export interface SearchBarProps extends FiltersProps { interface SearchBarOwnProps extends SearchBarProps { refresh: () => void; - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; datepickerOnly?: boolean; } diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx index e306f0458..58c569d45 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx @@ -23,6 +23,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); @@ -59,6 +60,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx index 642b56353..20a504a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx @@ -22,9 +22,14 @@ import { ThroughputPlt } from '../common/plots/throughput_plt'; import { SearchBar } from '../common/search_bar'; import { DashboardTable } from './dashboard_table'; -interface DashboardProps extends TraceAnalyticsComponentDeps {} +interface DashboardProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Dashboard(props: DashboardProps) { + const { appId, appName, page, parentBreadcrumb } = props; const [tableItems, setTableItems] = useState([]); const [throughputPltItems, setThroughputPltItems] = useState({ items: [], fixedInterval: '1h' }); const [errorRatePltItems, setErrorRatePltItems] = useState({ items: [], fixedInterval: '1h' }); @@ -34,19 +39,35 @@ export function Dashboard(props: DashboardProps) { const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, + const breadCrumbs = page === 'app' ? + [ { - text: 'Trace analytics', - href: '#/trace_analytics/home', + text: 'Application analytics', + href: '#/application_analytics', }, { - text: 'Dashboards', - href: '#/trace_analytics/home', + text: `${appName}`, + href: `#/application_analytics/${appId}`, }, + ] : [ + { + text: 'Trace analytics', + href: '#/trace_analytics/home', + }, + { + text: 'Dashboards', + href: '#/trace_analytics/home', + }, + ] + + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + ...breadCrumbs ]); - const validFilters = getValidFilterFields('dashboard'); + const validFilters = getValidFilterFields(page); props.setFilters([ ...props.filters.map((filter) => ({ ...filter, @@ -156,9 +177,13 @@ export function Dashboard(props: DashboardProps) { return ( <> + {page === 'app' ? + + :

Dashboard

+ } {props.indicesExist ? ( @@ -181,6 +206,7 @@ export function Dashboard(props: DashboardProps) { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={loading} + page="dashboard" /> diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx index 5fe49c4e2..a334868e8 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx @@ -33,6 +33,7 @@ export function DashboardTable(props: { addPercentileFilter: (condition?: 'gte' | 'lte', additionalFilters?: FilterType[]) => void; setRedirect: (redirect: boolean) => void; loading: boolean; + page: 'dashboard' | 'app'; }) { const getVarianceProps = (items: any[]) => { if (items.length === 0) { @@ -317,7 +318,7 @@ export function DashboardTable(props: { ), align: 'right', sortable: true, - render: (item, row) => ( + render: props.page === 'dashboard' ? (item, row) => ( { @@ -334,7 +335,7 @@ export function DashboardTable(props: { > - ), + ) : (item) => item }, ] as Array>; @@ -371,7 +372,7 @@ export function DashboardTable(props: { }; const varianceProps = useMemo(() => getVarianceProps(props.items), [props.items]); - const columns = useMemo(() => getColumns(), [props.items]); + const columns = useMemo(() => getColumns(), [props.items, props.filters]); const titleBar = useMemo(() => renderTitleBar(props.items?.length), [props.items]); const [sorting, setSorting] = useState<{ sort: PropertySort }>({ diff --git a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx index 706e618d5..e39fd4cc1 100644 --- a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx @@ -13,17 +13,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { ServicesTable } from './services_table'; -interface ServicesProps extends TraceAnalyticsComponentDeps {} +interface ServicesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Services(props: ServicesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -31,6 +44,12 @@ export function Services(props: ServicesProps) { text: 'Services', href: '#/trace_analytics/services', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('services'); props.setFilters([ @@ -71,9 +90,13 @@ export function Services(props: ServicesProps) { return ( <> + {page==='app' ? + + :

Services

+ } { endTime="now" setEndTime={setEndTime} indicesExist={false} + page="traces" /> ); @@ -59,6 +59,7 @@ describe('Traces component', () => { endTime="now" setEndTime={setEndTime} indicesExist={true} + page="traces" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx index f06e48d86..bfe159bc9 100644 --- a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx @@ -12,17 +12,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { TracesTable } from './traces_table'; -interface TracesProps extends TraceAnalyticsComponentDeps {} +interface TracesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'traces' | 'app'; +} export function Traces(props: TracesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -30,6 +43,12 @@ export function Traces(props: TracesProps) { text: 'Traces', href: '#/trace_analytics/traces', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('traces'); props.setFilters([ @@ -55,9 +74,13 @@ export function Traces(props: TracesProps) { return ( <> + {page === 'app' ? + + :

Traces

+ } diff --git a/dashboards-observability/public/components/trace_analytics/home.tsx b/dashboards-observability/public/components/trace_analytics/home.tsx index ea198c8e7..f68e0edce 100644 --- a/dashboards-observability/public/components/trace_analytics/home.tsx +++ b/dashboards-observability/public/components/trace_analytics/home.tsx @@ -88,7 +88,7 @@ export const Home = (props: HomeProps) => { path={['/trace_analytics', '/trace_analytics/home']} render={(routerProps) => ( - + )} /> @@ -97,7 +97,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/traces" render={(routerProps) => ( - + )} /> @@ -117,7 +117,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/services" render={(routerProps) => ( - + )} /> diff --git a/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts b/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts new file mode 100644 index 000000000..e69de29bb diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt new file mode 100644 index 000000000..d9f5f688d --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt @@ -0,0 +1,270 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Application main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "name": "Cool Application",
+ *   "description": "Application that includes multiple cool services",
+ *   "baseQuery": "source = opensearch_sample_database_flights",
+ *   "servicesEntities": [
+ *       "Payment",
+ *       "Users",
+ *       "Purchase"
+ *   ],
+ *   "traceGroups": [
+ *       "Payment.auto",
+ *       "Users.admin",
+ *       "Purchase.source"
+ *   ],
+ *   "availabilityLevels": [
+ *       {
+ *           "label": "Unavailable",
+ *           "color": "#D36086",
+ *           "condition": "when errorRate() is above or equal to 2%",
+ *           "order": "0",
+ *       }
+ *   ],
+ * }
+ * }
+ */ + +internal data class Application( + val name: String?, + val description: String?, + val baseQuery: String?, + val servicesEntities: List, + val traceGroups: List, + val availabilityLevels: List +) : BaseObjectData { + + internal companion object { + private val log by logger(Application::class.java) + private const val NAME_TAG = "name" + private const val DESCRIPTION_TAG = "description" + private const val BASE_QUERY_TAG = "baseQuery" + private const val SERVICES_ENTITIES_TAG = "servicesEntities" + private const val TRACE_GROUPS_TAG = "traceGroups" + private const val AVAILABILITY_LEVELS_TAG = "availabilityLevels" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Application(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the item list from parser + * @param parser data referenced at parser + * @return created list of items + */ + private fun parseItemList(parser: XContentParser): List { + val retList: MutableList = mutableListOf() + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser) + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + retList.add(AvailabilityLevel.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create ObservabilityObject object + * @param parser data referenced at parser + * @return created ObservabilityObject object + */ + fun parse(parser: XContentParser): Application { + var name: String? = null + var description: String? = null + var baseQuery: String? = null + var servicesEntities: List = listOf() + var traceGroups: List = listOf() + var availabilityLevels: List = listOf() + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + NAME_TAG -> name = parser.text() + DESCRIPTION_TAG -> description = parser.text() + BASE_QUERY_TAG -> baseQuery = parser.text() + SERVICES_ENTITIES_TAG -> servicesEntities = parser.stringList() + TRACE_GROUPS_TAG -> traceGroups = parser.stringList() + AVAILABILITY_LEVELS_TAG -> availabilityLevels = parseItemList(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Application Skipping Unknown field $fieldName") + } + } + } + return Application(name, description, baseQuery, servicesEntities, traceGroups, availabilityLevels) + } + } + + /** + * create XContentBuilder from this object using [XContentFactory.jsonBuilder()] + * @param params XContent parameters + * @return created XContentBuilder object + */ + fun toXContent(params: ToXContent.Params = ToXContent.EMPTY_PARAMS): XContentBuilder? { + return toXContent(XContentFactory.jsonBuilder(), params) + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + name = input.readString(), + description = input.readString(), + baseQuery = input.readString(), + servicesEntities = input.readStringList(), + traceGroups = input.readStringList(), + availabilityLevels = input.readList(AvailabilityLevel.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(description) + output.writeString(baseQuery) + output.writeStringCollection(servicesEntities) + output.writeStringCollection(traceGroups) + output.writeCollection(availabilityLevels) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DESCRIPTION_TAG, description) + .fieldIfNotNull(BASE_QUERY_TAG, baseQuery) + .fieldIfNotNull(SERVICES_ENTITIES_TAG, servicesEntities) + .fieldIfNotNull(TRACE_GROUPS_TAG, traceGroups) + .fieldIfNotNull(AVAILABILITY_LEVELS_TAG, availabilityLevels) + return builder.endObject() + } + + internal data class AvailabilityLevel( + val label: String?, + val color: String?, + val condition: String?, + val order: String? + ) : BaseModel { + internal companion object { + private const val LABEL_TAG = "label" + private const val COLOR_TAG = "color" + private const val CONDITION_TAG = "condition" + private const val ORDER_TAG = "order" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { AvailabilityLevel(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Trigger object + * @param parser data referenced at parser + * @return created Trigger object + */ + fun parse(parser: XContentParser): AvailabilityLevel { + var label: String? = null + var color: String? = null + var condition: String? = null + var order: String? = null + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + LABEL_TAG -> label = parser.text() + COLOR_TAG -> color = parser.text() + CONDITION_TAG -> condition = parser.text() + ORDER_TAG -> order = parser.text() + else -> log.info("$LOG_PREFIX: AvailabilityLevel Skipping Unknown field $fieldName") + } + } + label ?: throw IllegalArgumentException("$LABEL_TAG field absent") + color ?: throw IllegalArgumentException("$COLOR_TAG field absent") + condition ?: throw IllegalArgumentException("$CONDITION_TAG field absent") + order ?: throw IllegalArgumentException("$ORDER_TAG field absent") + return AvailabilityLevel(label, color, condition, order) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + label = input.readString(), + color = input.readString(), + condition = input.readString(), + order = input.readString(), + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(label) + output.writeString(color) + output.writeString(condition) + output.writeString(order) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(LABEL_TAG, label) + .field(COLOR_TAG, color) + .field(CONDITION_TAG, condition) + .field(ORDER_TAG, order) + builder.endObject() + return builder + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt index c9b4b6ce3..4da7f24bb 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt @@ -29,6 +29,10 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.OPERATIONAL_PANEL, ObjectProperty(OperationalPanel.reader, OperationalPanel.xParser) ), + Pair( + ObservabilityObjectType.APPLICATION, + ObjectProperty(Application.reader, Application.xParser) + ), Pair( ObservabilityObjectType.TIMESTAMP, ObjectProperty(Timestamp.reader, Timestamp.xParser) @@ -54,6 +58,7 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.SAVED_QUERY -> objectData is SavedQuery ObservabilityObjectType.SAVED_VISUALIZATION -> objectData is SavedVisualization ObservabilityObjectType.OPERATIONAL_PANEL -> objectData is OperationalPanel + ObservabilityObjectType.APPLICATION -> objectData is Application ObservabilityObjectType.TIMESTAMP -> objectData is Timestamp ObservabilityObjectType.NONE -> true } diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt index c5e4cd8e8..29aa51852 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.observability.model import org.opensearch.common.io.stream.StreamInput diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt index b1940af47..91e4edcd0 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt @@ -6,6 +6,7 @@ package org.opensearch.observability.model import org.opensearch.commons.utils.EnumParser +import org.opensearch.observability.model.RestTag.APPLICATION_FIELD import org.opensearch.observability.model.RestTag.NOTEBOOK_FIELD import org.opensearch.observability.model.RestTag.OPERATIONAL_PANEL_FIELD import org.opensearch.observability.model.RestTag.SAVED_QUERY_FIELD @@ -42,6 +43,11 @@ enum class ObservabilityObjectType(val tag: String) { return tag } }, + APPLICATION(APPLICATION_FIELD) { + override fun toString(): String { + return tag + } + }, TIMESTAMP(TIMESTAMP_FIELD) { override fun toString(): String { return tag diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt index 422582080..1bc390049 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt @@ -31,6 +31,7 @@ internal object RestTag { const val SAVED_QUERY_FIELD = "savedQuery" const val SAVED_VISUALIZATION_FIELD = "savedVisualization" const val OPERATIONAL_PANEL_FIELD = "operationalPanel" + const val APPLICATION_FIELD = "application" const val TIMESTAMP_FIELD = "timestamp" private val INCLUDE_ID = Pair(OBJECT_ID_FIELD, "true") private val EXCLUDE_ACCESS = Pair(ACCESS_LIST_FIELD, "false") diff --git a/opensearch-observability/src/main/resources/observability-mapping.yml b/opensearch-observability/src/main/resources/observability-mapping.yml index 4748204b8..beb9615bb 100644 --- a/opensearch-observability/src/main/resources/observability-mapping.yml +++ b/opensearch-observability/src/main/resources/observability-mapping.yml @@ -54,6 +54,14 @@ properties: fields: keyword: type: keyword + application: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword timestamp: type: object properties: From a45e9f360eefacc9686a330f2a866568a231e4dc Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 14 Dec 2021 11:56:03 -0800 Subject: [PATCH 11/16] Application Analytics (#299) * Add database schema for application Signed-off-by: Eugene Lee * Finished front end for Application overview Signed-off-by: Eugene Lee * Finished application detail page tabs Signed-off-by: Eugene Lee * WIP: Overview Page Signed-off-by: Eugene Lee * Rough sketch of App Analytics UI Signed-off-by: Eugene Lee * Create dummy page Signed-off-by: Eugene Lee * Create app complete. Stabilizing dashboard component. Signed-off-by: Eugene Lee * Update to 1.2 Observability Signed-off-by: Eugene Lee * notebooks internal error Signed-off-by: Eugene Lee * Address comments on PR: copyright headers, indentation, unnecessary render props Signed-off-by: Eugene Lee * Set max width of app and event Signed-off-by: Eugene Lee * Remove optional after description Signed-off-by: Eugene Lee * Change to singular Signed-off-by: Eugene Lee * Remove count badge for log source Signed-off-by: Eugene Lee * #290: Change form row label to ppl base query Signed-off-by: Eugene Lee * #291: Change description and help text for log source Signed-off-by: Eugene Lee * Pass down proper props Signed-off-by: Eugene Lee * Resolve gradle error and module not found error Signed-off-by: Eugene Lee * Resolve kotlin errors Signed-off-by: Eugene Lee * Fix parsers Signed-off-by: Eugene Lee * Add praseItemList Signed-off-by: Eugene Lee * Camelcase fields Signed-off-by: Eugene Lee * Remove whitespace, add copyright Signed-off-by: Eugene Lee * #292: Add autocomplete to Log Source accordion Signed-off-by: Eugene Lee * Lexicographic kotlin import Signed-off-by: Eugene Lee * Add newline at end of files Signed-off-by: Eugene Lee * #293: Add service map to create page Signed-off-by: Eugene Lee * #304: Activate Clear All button for services Signed-off-by: Eugene Lee * #305: Add button to clear base query Signed-off-by: Eugene Lee * opensearch-project#295: Add eui combo box for trace groups Signed-off-by: Eugene Lee * Separate out configuration renders Signed-off-by: Eugene Lee * debug adding filters traces Signed-off-by: Eugene Lee * #296: Add traces table to config Signed-off-by: Eugene Lee * Change from tsx to ts Signed-off-by: Eugene Lee * opensearch-project#309: Add page props and add app specific filters Signed-off-by: Eugene Lee * #308: Add button to clear trace groups Signed-off-by: Eugene Lee * #311: Allow services and traces to be selected Signed-off-by: Eugene Lee * Remove link to traces on table Signed-off-by: Eugene Lee * Disable clear all if nothing selected Signed-off-by: Eugene Lee * disable clear all when no log source Signed-off-by: Eugene Lee * Remove comment, add style to constant, temporarily remove availability Signed-off-by: Eugene Lee * Address PR comments Signed-off-by: Eugene Lee * Revert type assignment Signed-off-by: Eugene Lee * Update tests, builds and doc (#318) * rebased with bwc tests Signed-off-by: Shenoy Pratik * updated bwc tests Signed-off-by: Shenoy Pratik * added release notes Signed-off-by: Shenoy Pratik * Fix errors and address comments Signed-off-by: Eugene Lee * #319: Disable create until required fields are filled out Signed-off-by: Eugene Lee * #329: Add missing field tool tip Signed-off-by: Eugene Lee * Remove unnecessary imports Signed-off-by: Eugene Lee * #320: Add clear modal for friction Signed-off-by: Eugene Lee Co-authored-by: Shenoy Pratik --- opensearch-observability/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 7cd5c88a7..b68c36dc6 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,7 +9,11 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { +<<<<<<< HEAD opensearch_version = System.getProperty("opensearch.version", "1.2.3-SNAPSHOT") +======= + opensearch_version = System.getProperty("opensearch.version", "1.2.1-SNAPSHOT") +>>>>>>> b52bf65 (Application Analytics (#299)) // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -262,7 +266,11 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" +<<<<<<< HEAD versions = ["1.1.0",opensearch_version] +======= + versions = ["1.1.0","1.2.1-SNAPSHOT"] +>>>>>>> b52bf65 (Application Analytics (#299)) numberOfNodes = 3 plugin(provider(new Callable(){ @Override From b9607895671e41d9f1fb3d306f258a540e52969d Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 21 Dec 2021 15:46:48 -0800 Subject: [PATCH 12/16] Fix merge conflict Signed-off-by: Eugene Lee --- opensearch-observability/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index b68c36dc6..80d3a5cbe 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,11 +9,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { -<<<<<<< HEAD opensearch_version = System.getProperty("opensearch.version", "1.2.3-SNAPSHOT") -======= - opensearch_version = System.getProperty("opensearch.version", "1.2.1-SNAPSHOT") ->>>>>>> b52bf65 (Application Analytics (#299)) // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -266,11 +262,15 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" +<<<<<<< HEAD <<<<<<< HEAD versions = ["1.1.0",opensearch_version] ======= versions = ["1.1.0","1.2.1-SNAPSHOT"] >>>>>>> b52bf65 (Application Analytics (#299)) +======= + versions = ["1.1.0","1.2.2-SNAPSHOT"] +>>>>>>> 1fb9c9f (Fix merge conflict) numberOfNodes = 3 plugin(provider(new Callable(){ @Override From 4521c393c293b3ce76ccbddb11fe376cde49f3d5 Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 21 Dec 2021 15:47:25 -0800 Subject: [PATCH 13/16] #321: Add clear modal for services (#355) Signed-off-by: Eugene Lee --- .../config_components/service_config.tsx | 74 ++++++++++++++----- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx index 9afed9b4c..c6533d2ca 100644 --- a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx +++ b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiOverlayMask, EuiSpacer, EuiText } from "@elastic/eui"; import { FilterType } from "../../../trace_analytics/components/common/filters/filters"; import { ServiceObject } from "../../../trace_analytics/components/common/plots/service_map"; import { ServiceMap } from "../../../trace_analytics/components/services"; @@ -13,6 +13,7 @@ import React, { useState } from "react"; import { useEffect } from "react"; import { AppAnalyticsComponentDeps } from "../../home"; import { optionType } from "common/constants/application_analytics"; +import { getClearModal } from "../helpers/modal_containers"; interface ServiceConfigProps extends AppAnalyticsComponentDeps { dslService: DSLService; @@ -25,6 +26,8 @@ export const ServiceConfig = (props: ServiceConfigProps) => { const [servicesOpen, setServicesOpen] = useState(false); const [serviceMap, setServiceMap] = useState({}); const [serviceMapIdSelected, setServiceMapIdSelected] = useState<'latency' | 'error_rate' | 'throughput'>('latency'); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); useEffect(() => { handleServiceMapRequest(http, dslService, serviceMap, setServiceMap); @@ -71,26 +74,57 @@ export const ServiceConfig = (props: ServiceConfigProps) => { const services = Object.keys(serviceMap).map((service) => { return { label: service } }); + const onCancel = () => { + setIsModalVisible(false); + } + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onConfirm = () => { + clearServices(); + closeModal(); + } + + const clearAllModal = () => { + setModalLayout( + getClearModal( + onCancel, + onConfirm, + 'Clear services & entities?', + 'This will clear all information in services & entities configuration.', + 'Clear All' + ) + ); + showModal(); + }; + return ( +
- -

- Services & Entities {selectedServices.length} -

-
- - - Select services & entities to include in this application - - - } - extraAction={Clear all} - onToggle={(isOpen) => {setServicesOpen(isOpen)}} - paddingSize="l" - > + id="servicesEntities" + buttonContent={ + <> + +

+ Services & Entities {selectedServices.length} +

+
+ + + Select services & entities to include in this application + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setServicesOpen(isOpen)}} + paddingSize="l" + > @@ -112,5 +146,7 @@ export const ServiceConfig = (props: ServiceConfigProps) => { addFilter={addFilter} />
+ {isModalVisible && modalLayout} +
); } From c731141891d01ba7d32eb10059d89205c1ea575c Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 21 Dec 2021 15:47:38 -0800 Subject: [PATCH 14/16] #322: Add clear modal for traces (#356) Signed-off-by: Eugene Lee --- .../config_components/trace_config.tsx | 126 +++++++++++------- 1 file changed, 81 insertions(+), 45 deletions(-) diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx index e17fa78fd..6cbb6e537 100644 --- a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx +++ b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx @@ -4,7 +4,7 @@ */ import dateMath from '@elastic/datemath'; -import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiOverlayMask, EuiSpacer, EuiText } from "@elastic/eui"; import { optionType } from "common/constants/application_analytics"; import { filtersToDsl } from "../../../trace_analytics/components/common/helper_functions"; import { handleDashboardRequest } from "../../../trace_analytics/requests/dashboard_request_handler"; @@ -13,6 +13,7 @@ import React, { useEffect, useState } from "react"; import { AppAnalyticsComponentDeps } from "../../home"; import { DashboardTable } from '../../../trace_analytics/components/dashboard/dashboard_table'; import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; +import { getClearModal } from '../helpers/modal_containers'; interface TraceConfigProps extends AppAnalyticsComponentDeps { dslService: DSLService; @@ -28,6 +29,8 @@ export const TraceConfig = (props: TraceConfigProps) => { const [traceOptions, setTraceOptions] = useState>([]); const [percentileMap, setPercentileMap] = useState<{ [traceGroup: string]: number[] }>({}); const [redirect, setRedirect] = useState(true); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); useEffect(() => { setLoading(true) @@ -152,52 +155,85 @@ export const TraceConfig = (props: TraceConfigProps) => { setFilters(withoutTraces); }; + const onCancel = () => { + setIsModalVisible(false); + } + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onConfirm = () => { + clearTraces(); + closeModal(); + } + + const clearAllModal = () => { + setModalLayout( + getClearModal( + onCancel, + onConfirm, + 'Clear trace groups?', + 'This will clear all information in trace groups configuration.', + 'Clear all' + ) + ); + showModal(); + }; + return ( - - -

- Trace Groups {selectedTraces.length} -

-
- - - Constrain your application to specific trace groups - - - } - extraAction={Clear all} - onToggle={(isOpen) => {setTraceOpen(isOpen)}} - paddingSize="l" - > - + + +

+ Trace Groups {selectedTraces.length} +

+
+ + + Constrain your application to specific trace groups + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setTraceOpen(isOpen)}} + paddingSize="l" > - + +
+ + - - - -
+ + {isModalVisible && modalLayout} +
); } From fa8eceea27822a3e081a6c10875d7b1db4a7af4e Mon Sep 17 00:00:00 2001 From: Eugene Lee Date: Tue, 21 Dec 2021 16:20:50 -0800 Subject: [PATCH 15/16] Adds tests for application creation (#357) * Remove unnecessary memoization Signed-off-by: Eugene Lee * Add tests for create page Signed-off-by: Eugene Lee --- .../__snapshots__/create.test.tsx.snap | 11198 ++++++++++++++++ .../__snapshots__/log_config.test.tsx.snap | 786 ++ .../service_config.test.tsx.snap | 2105 +++ .../__snapshots__/trace_config.test.tsx.snap | 1545 +++ .../__tests__/create.test.tsx | 201 + .../__tests__/log_config.test.tsx | 85 + .../__tests__/service_config.test.tsx | 94 + .../__tests__/trace_config.test.tsx | 94 + .../components/application.tsx | 34 +- .../config_components/log_config.tsx | 86 +- 10 files changed, 16164 insertions(+), 64 deletions(-) create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/log_config.test.tsx.snap create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/trace_config.test.tsx.snap create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/create.test.tsx create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/log_config.test.tsx create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/service_config.test.tsx create mode 100644 dashboards-observability/public/components/application_analytics/__tests__/trace_config.test.tsx diff --git a/dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap b/dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap new file mode 100644 index 000000000..0a22d4fa8 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap @@ -0,0 +1,11198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create Page can clear query 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +