From 776d1edc1914f8fd33b2daa497956f5f389bd517 Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Wed, 24 May 2023 15:05:48 -0700 Subject: [PATCH] Updating 2.x from 2.7 changes (#482) * Fix for hover issue on docs of discover/explorer data grid (#329) (#358) Signed-off-by: Eric Wei (cherry picked from commit 19f29e1099b5e1785696fffa9830792bbad2e913) Co-authored-by: Eric Wei * [2.7] Refactor Saved objects and add visualization embeddable (#341) (#353) Signed-off-by: Joshua Li Co-authored-by: Eric Wei * Cohesion main to 2.7 (#382) Signed-off-by: Eric Wei Signed-off-by: Joshua Li Signed-off-by: Peter Fitzgibbons Signed-off-by: Shenoy Pratik Signed-off-by: Derek Ho Co-authored-by: Derek Ho Co-authored-by: Eric Wei Co-authored-by: Joshua Li Co-authored-by: Peter Fitzgibbons Co-authored-by: Shenoy Pratik Co-authored-by: Rupal Mahajan * Fix redirection to legacy event_analytics URL (#383) (#384) Signed-off-by: Joshua Li (cherry picked from commit 23e09e54f86a205443d78ef900b554a4fa66e4a0) Co-authored-by: Joshua Li * [2.7] Redo pick from main to 2.7 (#385) Signed-off-by: Peter Fitzgibbons Signed-off-by: Derek Ho Signed-off-by: Eric Wei Signed-off-by: Rupal Mahajan Signed-off-by: Kavitha Conjeevaram Mohan Signed-off-by: Shenoy Pratik Signed-off-by: Joshua Li Co-authored-by: Eric Wei Co-authored-by: Peter Fitzgibbons Co-authored-by: Derek Ho Co-authored-by: Peter Fitzgibbons Co-authored-by: Rupal Mahajan Co-authored-by: Kavitha Conjeevaram Mohan Co-authored-by: Shenoy Pratik * Metrics analytics support for SOpanels (#386) (#388) Signed-off-by: Shenoy Pratik (cherry picked from commit 1ecd7ea3d9d77d644e14adbba0b6769690e99046) Co-authored-by: Shenoy Pratik * change plugin ordering (#389) (#390) Signed-off-by: Shenoy Pratik (cherry picked from commit a6c1ebc03c70d91e497367f6dc490718f6624a2d) Co-authored-by: Shenoy Pratik * notebooks fix for cypress and minor bugs (#392) (#393) * notebooks fix for cypress and minor bugs Signed-off-by: Shenoy Pratik * remove comments Signed-off-by: Shenoy Pratik --------- Signed-off-by: Shenoy Pratik (cherry picked from commit 3219f143515e53e7e9d82ee4ee962adf6d820b4b) * Update traces integration tests (#391) (#397) * Update traces cypress tests Signed-off-by: Rupal Mahajan * Update traces cypress test Signed-off-by: Rupal Mahajan * Fix traces cypress tests Signed-off-by: Rupal Mahajan * Fix filters test Signed-off-by: Rupal Mahajan --------- Signed-off-by: Rupal Mahajan (cherry picked from commit b8f9ca38db4e368d776b11e2c0e431c9165f656e) Co-authored-by: Rupal Mahajan * Fix redirection to legacy event_analytics URL from dashboards (#399) (#403) Signed-off-by: Joshua Li (cherry picked from commit 05ef1806c44c91ff1099f6f23ec2176005643873) Co-authored-by: Joshua Li * Adjust metrics top menu layout to avoid overflow (#398) (#402) Signed-off-by: Joshua Li (cherry picked from commit 6309e635beb5b3463a2429e6b779155330895897) Co-authored-by: Joshua Li * Support duplicate visualization in dashboard (#400) (#405) Signed-off-by: Joshua Li (cherry picked from commit 3d74bacd8431ee6315dadc3b82463a2d958d1032) Co-authored-by: Joshua Li * fix panel visualization preview, new viz workflow (#401) (#404) Signed-off-by: Shenoy Pratik (cherry picked from commit 78347d9015848dea729c82fbbb4b1f69ebf5c686) Co-authored-by: Shenoy Pratik * Fix saving multiple metrics to SOpanels (#407) (#408) * fix saving multiple metrics to SOpanels Signed-off-by: Shenoy Pratik * remove unused variable Signed-off-by: Shenoy Pratik --------- Signed-off-by: Shenoy Pratik (cherry picked from commit a4cb6d1d33d00349449932f4679144acba963ace) Co-authored-by: Shenoy Pratik * Panel table fixes (#406) * Fix Panel View (legacy) - Duplicate - Rename - Delete --------- Signed-off-by: Peter Fitzgibbons Co-authored-by: Peter Fitzgibbons Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Co-authored-by: Joshua Li Co-authored-by: Shenoy Pratik * Release Notes 2.7.0.0 (#394) (#412) * Release Notes 2.7.0.0 Signed-off-by: Peter Fitzgibbons * Release Notes 2.7.0.0 Signed-off-by: Peter Fitzgibbons --------- Signed-off-by: Peter Fitzgibbons Co-authored-by: Peter Fitzgibbons (cherry picked from commit 969e6397d6187659edafe3a5df64b4cee3d1019d) Co-authored-by: Peter Fitzgibbons * [Dashboard listing] update edit url and view url (#416) (#421) * [Dashboard listing] update edit url and view url Update to include BASE_URL if basePath is defined. Since create url is just a direct nav and needs the basePath when passing props to the provider, the source code no longer appends the basePath to keep these URLs consistent. A feature should consolidate the basePaths in the dashboard listing service so that createUrl also adds the base path. At which point we no longer need the base path. Also removed some rogue URLs. Core PR: https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3922 --------- Signed-off-by: Kawika Avilla Signed-off-by: Joshua Li Co-authored-by: Joshua Li (cherry picked from commit f94c21e82cd29c55af6073d366d240328c684710) Co-authored-by: Kawika Avilla * Fix invalid time range in new panels (#419) Signed-off-by: Joshua Li * open new tab for editing panel visualization (#430) Signed-off-by: Eric Wei * Save new prometheus metrics to OSD saved objects (#428) * Save new prometheus metrics to OSD saved objects Signed-off-by: Joshua Li * Return if failed at saving metrics Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li * fixes edit mode callback bug in Saved Object Panels (#420) * fix for SOpanels editMode Signed-off-by: Shenoy Pratik * rename edit button data-test-subj Signed-off-by: Shenoy Pratik --------- Signed-off-by: Shenoy Pratik * Add Toasts to Observability Dashboards (#435) * Fixes * Panel View (legacy) - Duplicate - Rename Signed-off-by: Peter Fitzgibbons * Toasts use hook from useOpenSearchDashboards context provider Signed-off-by: Peter Fitzgibbons * Testing for CustomPanel Toast Signed-off-by: Peter Fitzgibbons * update catches from comments, minor code cleaning Signed-off-by: Shenoy Pratik * update tests Signed-off-by: Shenoy Pratik * remove unused redux slice Signed-off-by: Shenoy Pratik * revert cypress changes Signed-off-by: Shenoy Pratik * add toasts to SOflyout Signed-off-by: Shenoy Pratik * fix messaging for multiple delete Signed-off-by: Derek Ho * fix up toast and error handling for create and delete flows Signed-off-by: Derek Ho * fix up clone Signed-off-by: Derek Ho * fix rename in table Signed-off-by: Derek Ho * fix rename in custom panel so view Signed-off-by: Derek Ho * fix up panel toasts Signed-off-by: Derek Ho * fix up for flyout Signed-off-by: Derek Ho * code cleanup Signed-off-by: Derek Ho * finish merge Signed-off-by: Derek Ho * fix up PR comments Signed-off-by: Derek Ho --------- Signed-off-by: Peter Fitzgibbons Signed-off-by: Shenoy Pratik Signed-off-by: Derek Ho Co-authored-by: Peter Fitzgibbons Co-authored-by: Derek Ho * change samples to saved object (#427) (#436) * change samples to saved object * add sample to so * fix saved object sample * fix up * add back toast * revert file * refactoring * use constant --------- (cherry picked from commit ce06ccb9fc4acd75d3ccbfbacab9db8a521de252) Signed-off-by: Derek Ho * Add Error handling on duplicate dashboard names (#441) * Add check for existing panel name Signed-off-by: Rupal Mahajan * Nit Signed-off-by: Rupal Mahajan * Remove double await Signed-off-by: Rupal Mahajan * Add rename name check test Signed-off-by: Rupal Mahajan * Remove test Signed-off-by: Rupal Mahajan * Add unique id to sample panel name Signed-off-by: Rupal Mahajan --------- Signed-off-by: Rupal Mahajan * fix timestamp overriding issue (#449) Signed-off-by: Eric Wei * Renaming custom panels to Observability Dashboards (#450) * change custom panels -> observability dashboards Signed-off-by: Eric Wei * rest of the panel renaming Signed-off-by: Eric Wei --------- Signed-off-by: Eric Wei * Fix delete failure when no osd item selected (#452) Signed-off-by: Joshua Li (cherry picked from commit a4a310e49aa5feddc47fb2bf572bf232c80465bb) * Open log patterns by default (#444) (#459) Signed-off-by: Joshua Li (cherry picked from commit 72970aa5af30f6ea7c07271375feae67b11d8b6c) * diff merging between 2.x and 2.7 Signed-off-by: Shenoy Pratik * add service fields length check in app analytics Signed-off-by: Shenoy Pratik --------- Signed-off-by: Joshua Li Signed-off-by: Eric Wei Signed-off-by: Peter Fitzgibbons Signed-off-by: Shenoy Pratik Signed-off-by: Derek Ho Signed-off-by: Rupal Mahajan Signed-off-by: Kavitha Conjeevaram Mohan Co-authored-by: Joshua Li Co-authored-by: Eric Wei Co-authored-by: Peter Fitzgibbons Co-authored-by: Derek Ho Co-authored-by: Peter Fitzgibbons Co-authored-by: Rupal Mahajan Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Co-authored-by: Kavitha Conjeevaram Mohan Co-authored-by: Kawika Avilla --- .cypress/.DS_Store | Bin 0 -> 6148 bytes .cypress/integration/2_notebooks.spec.js | 37 +- .../{3_panels.spec.js => 3_panels.spec.ts} | 411 +- .cypress/integration/7_app_analytics.spec.js | 4 +- .cypress/plugins/index.js | 4 + .cypress/support/index.js | 2 + README.md | 6 +- common/constants/custom_panels.ts | 87 +- common/constants/shared.ts | 28 +- common/types/custom_panels.ts | 37 +- common/types/explorer.ts | 4 +- cypress.json | 5 +- package.json | 1 + public/components/app.tsx | 149 +- .../components/app_table.tsx | 20 +- .../components/application.tsx | 24 +- .../components/configuration.tsx | 4 +- .../components/create.tsx | 13 +- .../service_detail_flyout.tsx | 5 +- .../components/application_analytics/home.tsx | 110 +- public/components/common/side_nav.tsx | 145 - public/components/common/toast/index.tsx | 37 + .../custom_panel_table.test.tsx.snap | 3592 +++------ .../custom_panel_view.test.tsx.snap | 6574 +++++++++-------- .../__tests__/custom_panel_table.test.tsx | 104 +- .../__tests__/custom_panel_view.test.tsx | 82 +- .../custom_panels/custom_panel_table.tsx | 155 +- .../custom_panels/custom_panel_view.tsx | 132 +- .../custom_panels/custom_panel_view_so.tsx | 690 ++ .../helpers/__tests__/utils.test.tsx | 52 +- .../helpers/add_visualization_helper.ts | 125 + .../helpers/add_visualization_popover.tsx | 10 +- .../helpers/modal_containers.tsx | 4 +- .../helpers/panel_state_reducer.ts | 15 + .../custom_panels/helpers/utils.tsx | 36 +- public/components/custom_panels/home.tsx | 373 +- .../panel_modules/empty_panel.tsx | 2 +- .../panel_grid/panel_grid_so.tsx | 219 + .../visualization_flyout.tsx | 13 +- .../visualization_flyout_so.tsx | 424 ++ .../custom_panels/redux/panel_slice.ts | 342 + .../explorer/events_views/docViewer.tsx | 3 +- .../explorer/events_views/doc_flyout.tsx | 18 +- .../events_views/surrounding_flyout.tsx | 68 +- .../event_analytics/explorer/explorer.tsx | 2 +- .../event_analytics/explorer/log_explorer.tsx | 7 +- .../explorer/log_patterns/log_patterns.tsx | 36 +- .../__snapshots__/save_panel.test.tsx.snap | 1288 ++-- .../save_panel/__tests__/save_panel.test.tsx | 56 +- .../explorer/save_panel/save_panel.tsx | 26 +- .../components/event_analytics/home/home.tsx | 13 +- public/components/event_analytics/index.tsx | 39 +- public/components/index.tsx | 4 +- public/components/metrics/index.tsx | 167 +- .../__snapshots__/searchbar.test.tsx.snap | 8 +- .../components/metrics/sidebar/search_bar.tsx | 2 +- .../__snapshots__/top_menu.test.tsx.snap | 5709 +++++++------- .../components/metrics/top_menu/top_menu.scss | 16 +- .../components/metrics/top_menu/top_menu.tsx | 221 +- .../__snapshots__/note_table.test.tsx.snap | 16 +- .../__tests__/legacy_route_helpers.test.ts | 8 +- .../helpers/legacy_route_helpers.ts | 8 +- .../helpers/reporting_context_menu_helper.tsx | 3 +- .../components/notebooks/components/main.tsx | 45 +- .../notebooks/components/note_table.tsx | 15 +- .../notebooks/components/notebook.tsx | 18 +- .../paragraph_components/para_input.tsx | 3 +- .../paragraph_components/para_output.tsx | 3 +- .../components/common/legacy_route_helpers.ts | 8 +- .../__snapshots__/dashboard.test.tsx.snap | 30 +- .../dashboard/dashboard_content.tsx | 43 +- .../components/dashboard/dashboard_table.tsx | 3 +- .../dashboard/top_error_rates_table.tsx | 2 +- .../dashboard/top_latency_table.tsx | 3 +- .../__snapshots__/services.test.tsx.snap | 44 +- .../services/__tests__/services.test.tsx | 8 +- .../components/services/service_view.tsx | 12 +- .../components/services/services_content.tsx | 37 +- .../__snapshots__/traces.test.tsx.snap | 44 +- .../traces/__tests__/traces.test.tsx | 8 +- .../components/traces/trace_view.tsx | 8 +- .../components/traces/traces_content.tsx | 30 +- public/components/trace_analytics/home.tsx | 215 +- .../requests/dashboard_request_handler.ts | 5 +- .../trace_analytics/trace_side_nav.tsx | 74 + .../observability_embeddable_factory.tsx | 6 +- public/framework/core_refs.ts | 33 + public/framework/redux/reducers/index.ts | 2 + public/index.ts | 1 + public/plugin.ts | 164 +- .../data_fetchers/ppl/ppl_data_fetcher.ts | 4 +- .../saved_objects_actions.ts | 2 +- .../ppl/save_as_current_vis.ts | 11 +- .../ppl/save_as_new_query.ts | 5 +- .../ppl/save_as_new_vis.ts | 20 +- .../custom_panels/custom_panel_adaptor.ts | 3 +- .../helpers/custom_panels/sample_panels.ts | 80 - server/plugin.ts | 37 + server/routes/custom_panels/panels_router.ts | 43 + .../observability_saved_object.ts | 6 +- test/panels_constants.tsx | 2 +- test/setup.jest.ts | 4 + yarn.lock | 103 +- 103 files changed, 11882 insertions(+), 11097 deletions(-) create mode 100644 .cypress/.DS_Store rename .cypress/integration/{3_panels.spec.js => 3_panels.spec.ts} (57%) delete mode 100644 public/components/common/side_nav.tsx create mode 100644 public/components/common/toast/index.tsx create mode 100644 public/components/custom_panels/custom_panel_view_so.tsx create mode 100644 public/components/custom_panels/helpers/add_visualization_helper.ts create mode 100644 public/components/custom_panels/helpers/panel_state_reducer.ts create mode 100644 public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx create mode 100644 public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx create mode 100644 public/components/custom_panels/redux/panel_slice.ts create mode 100644 public/components/trace_analytics/trace_side_nav.tsx create mode 100644 public/framework/core_refs.ts delete mode 100644 server/common/helpers/custom_panels/sample_panels.ts diff --git a/.cypress/.DS_Store b/.cypress/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ab1b61f97e8de91e1f5ea7ca39c7e9ac4e976bce GIT binary patch literal 6148 zcmeHKJ8Hu~5S?*c2-K)dxmU;y79pR&7f2vM7=jGOPOU2E%F*)XLqNzWT(~iBVCL=4 z&Rd~ZXfz_C{pI~dq!W<_ZYbXt)@JACE8Ap7fpFY0%1PejEC)HRs;?)E+oFB~>tn&6 zzV@f-^Ei!h`*k|ARDcRl0V+TRsKAX1Snq{3Z6G5RpaN9jPXYTr6u4ncoCE#Sf#4$m zaEr7X);>!BizR?HaSlWVra=V;RddA9pd(+ht|rcbK^M*8L-Wa+6N>uNaewi0(Hh7| z1*pJMfv4DZ*8g|#f9C%si90Gl1s+NP-46Sq$17!TUA>(3+5&%tTg?yL3~Q$#csmAq iJI2P^@v|32U9mOJYvLT}bmW~5| literal 0 HcmV?d00001 diff --git a/.cypress/integration/2_notebooks.spec.js b/.cypress/integration/2_notebooks.spec.js index 5743cd07af..bd78ef24ae 100644 --- a/.cypress/integration/2_notebooks.spec.js +++ b/.cypress/integration/2_notebooks.spec.js @@ -21,17 +21,26 @@ import { SAMPLE_PANEL } from '../utils/panel_constants'; import { skipOn } from '@cypress/skip-test'; const moveToEventsHome = () => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/event_analytics/`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/`); cy.wait(delay * 3); }; const moveToPanelHome = () => { - cy.visit( - `${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/operational_panels/` - ); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/`); cy.wait(delay * 3); }; +const moveToTestNotebook = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-notebooks#/`, { + timeout: 6000, + }); + cy.get('.euiTableCellContent') + .contains(TEST_NOTEBOOK, { + timeout: 6000, + }) + .click(); +}; + describe('Adding sample data and visualization', () => { it('Adds sample flights data for visualization paragraph', () => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/home#/tutorial_directory/sampleData`); @@ -41,20 +50,20 @@ describe('Adding sample data and visualization', () => { }); it('Add sample observability data', () => { - moveToPanelHome(); - cy.get('.euiButton__text').contains('Actions').trigger('mouseover').click(); + moveToEventsHome(); + cy.get('button[data-test-subj="eventHomeAction"]').trigger('mouseover').click(); cy.wait(100); - cy.get('.euiContextMenuItem__text').contains('Add samples').trigger('mouseover').click(); + cy.get('button[data-test-subj="eventHomeAction__addSamples"]').trigger('mouseover').click(); cy.wait(100 * 3); cy.get('.euiModalHeader__title[data-test-subj="confirmModalTitleText"]') .contains('Add samples') .should('exist'); cy.wait(100); - cy.get('.euiButton__text').contains('Yes').trigger('mouseover').click(); + cy.get('button[data-test-subj="confirmModalConfirmButton"]').trigger('mouseover').click(); cy.wait(100 * 5); cy.route2('POST', '/addSamplePanels').as('addSamples'); cy.wait('@addSamples').then(() => { - cy.get('.euiTableCellContent').contains(SAMPLE_PANEL).should('exist'); + cy.get('.euiToastHeader__title').should('contain', 'successfully'); }); cy.wait(100); }); @@ -62,7 +71,8 @@ describe('Adding sample data and visualization', () => { describe('Testing notebooks table', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-notebooks#/`); + cy.wait(delay); }); it('Notebooks table empty state', () => { @@ -178,7 +188,7 @@ describe('Testing notebooks table', () => { describe('Test reporting integration if plugin installed', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-notebooks#/`); cy.get('.euiTableCellContent').contains(TEST_NOTEBOOK).click(); cy.wait(delay * 3); cy.get('body').then(($body) => { @@ -222,8 +232,7 @@ describe('Test reporting integration if plugin installed', () => { describe('Testing paragraphs', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); - cy.get('.euiTableCellContent').contains(TEST_NOTEBOOK).click(); + moveToTestNotebook(); }); it('Goes into a notebook and creates paragraphs', () => { @@ -550,7 +559,5 @@ describe('clean up all test data', () => { }); cy.get('button.euiButton--danger').should('not.be.disabled'); cy.get('.euiButton__text').contains('Delete').trigger('mouseover').click(); - - cy.get('.euiTextAlign').contains('No Operational Panels').should('exist'); }); }); diff --git a/.cypress/integration/3_panels.spec.js b/.cypress/integration/3_panels.spec.ts similarity index 57% rename from .cypress/integration/3_panels.spec.js rename to .cypress/integration/3_panels.spec.ts index 900472fa27..1e8a3ff2f3 100644 --- a/.cypress/integration/3_panels.spec.js +++ b/.cypress/integration/3_panels.spec.ts @@ -5,38 +5,16 @@ /// +import { suppressResizeObserverIssue } from '../utils/constants'; import { delay, - TEST_PANEL, - PPL_VISUALIZATIONS, - PPL_VISUALIZATIONS_NAMES, NEW_VISUALIZATION_NAME, PPL_FILTER, - SAMPLE_PANEL, - SAMPLE_VISUALIZATIONS_NAMES, + PPL_VISUALIZATIONS, + PPL_VISUALIZATIONS_NAMES, + TEST_PANEL, } from '../utils/panel_constants'; -import { suppressResizeObserverIssue } from '../utils/constants'; - -const moveToEventsHome = () => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/event_analytics/`); - cy.wait(delay * 3); -}; - -const moveToPanelHome = () => { - cy.visit( - `${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/operational_panels/` - ); - cy.wait(delay * 3); -}; - -const moveToTestPanel = () => { - moveToPanelHome(); - cy.get('.euiTableCellContent').contains(TEST_PANEL).trigger('mouseover').click(); - cy.wait(delay * 3); - cy.get('h1').contains(TEST_PANEL).should('exist'); -}; - describe('Adding sample data and visualization', () => { it('Adds sample flights data for visualization paragraph', () => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/home#/tutorial_directory/sampleData`); @@ -98,83 +76,181 @@ describe('Creating visualizations', () => { describe('Testing panels table', () => { beforeEach(() => { + eraseTestPanels(); moveToPanelHome(); }); - it('Displays error toast for invalid panel name', () => { - cy.get('button[data-test-subj="customPanels__createNewPanels"]').click(); - cy.get('button[data-test-subj="runModalButton"]').click(); - cy.get('.euiToastHeader__title').contains('Invalid Operational Panel name').should('exist'); - }); + describe('Without Any Panels', () => { + beforeEach(() => { + moveToPanelHome(); + }); - it('Creates a panel and redirects to the panel', () => { - cy.get('button[data-test-subj="customPanels__createNewPanels"]').click(); - cy.get('input.euiFieldText').focus().type(TEST_PANEL, { - delay: 50, + it.skip('Displays error toast for invalid panel name', () => { + clickCreatePanelButton(); + confirmModal(); + expectToastWith('Invalid Dashboard name'); }); - cy.get('button[data-test-subj="runModalButton"]').click(); - cy.contains(TEST_PANEL).should('exist'); - }); - it('Duplicates a panel', () => { - cy.get('.euiCheckbox__input[title="Select this row"]').eq(0).trigger('mouseover').click(); - cy.get('button[data-test-subj="operationalPanelsActionsButton"]').click(); - cy.get('button[data-test-subj="duplicateContextMenuItem"]').click(); - cy.get('button[data-test-subj="runModalButton"]').click(); + it('Creates a panel and redirects to the panel', () => { + clickCreatePanelButton(); + cy.get('input.euiFieldText').focus().type(TEST_PANEL, { + delay: 50, + }); + cy.get('button[data-test-subj="runModalButton"]').click(); + cy.contains(TEST_PANEL).should('exist'); + }); }); - it('Renames a panel', () => { - cy.get('.euiCheckbox__input[title="Select this row"]').eq(0).trigger('mouseover').click(); - cy.get('button[data-test-subj="operationalPanelsActionsButton"]').click(); - cy.get('button[data-test-subj="renameContextMenuItem"]').click(); - cy.get('input.euiFieldText').focus().type(' (rename)', { - delay: 50, + describe('with a Legacy Panel', () => { + beforeEach(() => { + createLegacyPanel(); + moveToPanelHome(); }); - cy.get('button[data-test-subj="runModalButton"]').click(); - }); - it('Searches existing panel', () => { - cy.get('input[data-test-subj="operationalPanelSearchBar"]') - .focus() - .type('this panel should not exist', { + it('Duplicates a legacy panel', () => { + cy.get('.euiTableRow').should('have.length', 1); + selectThePanel(); + openActionsDropdown(); + cy.get('button[data-test-subj="duplicateContextMenuItem"]').click(); + cy.get('button[data-test-subj="runModalButton"]').click(); + cy.get('.euiTableRow').should('have.length', 2); + const duplicateName = TEST_PANEL + ' (copy)'; + cy.contains(duplicateName).should('exist'); + const duplicate = cy.get('.euiLink').contains(duplicateName); + expectUuid(duplicate); + }); + + it('Renames the panel', () => { + createLegacyPanel(); + cy.reload(); + const cell = cy.get('.euiTableCellContent'); + expectLegacyId(cell); + selectThePanel(); + openActionsDropdown(); + cy.get('button[data-test-subj="renameContextMenuItem"]').click(); + cy.get('input.euiFieldText').focus().type(' (rename)'); + cy.get('button[data-test-subj="runModalButton"]').click(); + const renamed = testPanelTableCell(); + expectUuid(renamed); + }); + + it('Deletes the panel', () => { + cy.get('input[data-test-subj="checkboxSelectAll"]').click(); + openActionsDropdown(); + cy.get('button[data-test-subj="deleteContextMenuItem"]').click(); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('be.disabled'); + + cy.get('input.euiFieldText[placeholder="delete"]').focus().type('delete', { delay: 50, }); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('not.be.disabled'); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').click(); + cy.get('h2[data-test-subj="customPanels__noPanelsHome"]').should('exist'); + }); - cy.get('.euiTableCellContent__text').contains('No items found').should('exist'); + it('Searches panels', () => { + createLegacyPanel('Legacy Named'); + createSavedObjectPanel('Saved Object'); + cy.reload(); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type('this panel should not exist', { + delay: 50, + }); + + cy.get('.euiTableCellContent__text').contains('No items found').should('exist'); + + // Search for oriignal Legacy Panel + cy.get('[aria-label="Clear input"]').click(); + cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(TEST_PANEL, { + delay: 50, + }); - cy.get('[aria-label="Clear input"]').click(); - cy.get('input[data-test-subj="operationalPanelSearchBar"]') - .focus() - .type(TEST_PANEL + ' (copy) (rename)', { + cy.get('a.euiLink').contains(TEST_PANEL).should('exist'); + cy.get('.euiTableRow').should('have.length', 1); + + // Search for teh Saved Object panel + cy.get('[aria-label="Clear input"]').click(); + cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type('Saved Object', { delay: 50, }); - cy.get('a.euiLink') - .contains(TEST_PANEL + ' (copy) (rename)') - .should('exist'); + cy.get('a.euiLink').contains('Saved Object').should('exist'); + cy.get('.euiTableRow').should('have.length', 1); + }); }); - it('Deletes panels', () => { - cy.get('input[data-test-subj="checkboxSelectAll"]').click(); - cy.get('button[data-test-subj="operationalPanelsActionsButton"]').click(); - cy.get('button[data-test-subj="deleteContextMenuItem"]').click(); - cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('be.disabled'); + describe('with a SavedObjects Panel', () => { + beforeEach(() => { + createSavedObjectPanel(); + moveToPanelHome(); + cy.get('.euiTableRow').should('have.length', 1); + }); - cy.get('input.euiFieldText[placeholder="delete"]').focus().type('delete', { - delay: 50, + it('Duplicates the panel', () => { + selectThePanel(); + openActionsDropdown(); + cy.get('button[data-test-subj="duplicateContextMenuItem"]').click(); + cy.get('button[data-test-subj="runModalButton"]').click(); + const duplicateName = TEST_PANEL + ' (copy)'; + cy.get('.euiTableRow').should('have.length', 2); + cy.contains(duplicateName).should('exist'); + const duplicate = cy.get('.euiLink').contains(duplicateName); + expectUuid(duplicate); }); - cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('not.be.disabled'); - cy.get('button[data-test-subj="popoverModal__deleteButton"]').click(); - cy.get('h2[data-test-subj="customPanels__noPanelsHome"]').should('exist'); - }); - it('Create a panel for testing', () => { - // keep a panel for testing - cy.get('button[data-test-subj="customPanels__createNewPanels"]').click(); - cy.get('input.euiFieldText').focus().type(TEST_PANEL, { - delay: 50, + it('Renames a saved-objects panel', () => { + selectThePanel(); + openActionsDropdown(); + cy.get('button[data-test-subj="renameContextMenuItem"]').click(); + cy.get('input.euiFieldText').focus().type(' (rename)', { + delay: 50, + }); + cy.get('button[data-test-subj="runModalButton"]').click(); + }); + + it('Deletes the panel', () => { + createSavedObjectPanel(); + cy.get('input[data-test-subj="checkboxSelectAll"]').click(); + openActionsDropdown(); + cy.get('button[data-test-subj="deleteContextMenuItem"]').click(); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('be.disabled'); + + cy.get('input.euiFieldText[placeholder="delete"]').focus().type('delete', { + delay: 50, + }); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').should('not.be.disabled'); + cy.get('button[data-test-subj="popoverModal__deleteButton"]').click(); + cy.get('h2[data-test-subj="customPanels__noPanelsHome"]').should('exist'); + }); + + it('Redirects to observability dashboard from OSD dashboards', () => { + moveToOsdDashboards(); + cy.location('pathname').should('eq', '/app/dashboards'); + cy.get('[data-test-subj="dashboardListingTitleLink-Test-Panel"]').click(); + cy.location('pathname').should('eq', '/app/observability-dashboards'); + }); + + it('Redirects to observability dashboard from OSD dashboards with edit', () => { + moveToOsdDashboards(); + cy.location('pathname').should('eq', '/app/dashboards'); + cy.get('[data-test-subj="dashboardListingTitleLink-Test-Panel"]') + .closest('tr') + .get('span.euiToolTipAnchor > button.euiButtonIcon') + .eq(0) + .click(); + cy.location('pathname').should('eq', '/app/observability-dashboards'); + cy.location('hash').should('include', '/edit'); + }); + + it('Redirects to observability dashboard from OSD dashboards with create', () => { + moveToOsdDashboards(); + cy.location('pathname').should('eq', '/app/dashboards'); + cy.get('div#createMenuPopover').click(); + cy.get('[data-test-subj="contextMenuItem-observability-panel"]').click(); + cy.location('pathname').should('eq', '/app/observability-dashboards'); + cy.location('hash').should('include', '/create'); }); - cy.get('button[data-test-subj="runModalButton"]').click(); }); }); @@ -194,9 +270,9 @@ describe('Testing a panel', () => { cy.get('a[data-test-subj="breadcrumb last"]').click(); cy.get('h1[data-test-subj="panelNameHeader"]').contains(TEST_PANEL).should('exist'); - cy.get('a[data-test-subj="breadcrumb"]').contains('Operational panels').click(); + cy.get('a[data-test-subj="breadcrumb"]').contains('Observability Dashboards').click(); - cy.get('a[data-test-subj="breadcrumb last"]').contains('Operational panels').should('exist'); + cy.get('a[data-test-subj="breadcrumb last"]').contains('Observability Dashboards').should('exist'); cy.get('a[data-test-subj="breadcrumb first"]').contains('Observability').click(); cy.get('a[data-test-subj="breadcrumb"]').contains('Event analytics').should('exist'); @@ -470,8 +546,8 @@ describe('Clean up all test data', () => { it('Deletes test panel', () => { moveToPanelHome(); cy.get('.euiCheckbox__input[data-test-subj="checkboxSelectAll"]').trigger('mouseover').click(); - cy.get('button[data-test-subj="operationalPanelsActionsButton"]').click(); - cy.get('button[data-test-subj="deleteContextMenuItem"]').click(); + openActionsDropdown(); + clickDeleteAction(); cy.get('button.euiButton--danger').should('be.disabled'); cy.get('input.euiFieldText[placeholder="delete"]').focus().type('delete', { delay: 50, @@ -479,6 +555,171 @@ describe('Clean up all test data', () => { cy.get('button.euiButton--danger').should('not.be.disabled'); cy.get('button[data-test-subj="popoverModal__deleteButton"]').click(); - cy.get('.euiTextAlign').contains('No Operational Panels').should('exist'); + cy.get('.euiTextAlign').contains('No Observability Dashboards').should('exist'); }); }); + +const moveToOsdDashboards = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/dashboards#/`); + cy.wait(delay * 3); +}; + +const moveToEventsHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/`); + cy.wait(6000); +}; + +const moveToPanelHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/`, { + timeout: 3000, + }); + cy.wait(delay * 3); +}; + +const testPanelTableCell = (name = TEST_PANEL) => cy.get('.euiTableCellContent').contains(name); + +const moveToTestPanel = () => { + moveToPanelHome(); + testPanelTableCell().trigger('mouseover').click(); + cy.wait(delay * 3); + cy.get('h1').contains(TEST_PANEL).should('exist'); +}; + +const TEST_PANEL_RX = new RegExp(TEST_PANEL + '.*'); + +const eraseLegacyPanels = () => { + cy.request({ + method: 'GET', + failOnStatusCode: false, + url: 'api/observability/operational_panels/panels', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }).then((response) => { + response.body.panels.map((panel) => { + cy.request({ + method: 'DELETE', + failOnStatusCode: false, + url: `api/observability/operational_panels/panels/${panel.id}`, + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }).then((response) => { + const deletedId = response.allRequestResponses[0]['Request URL'].split('/').slice(-1); + console.log('erased panel', deletedId); + }); + }); + }); +}; + +const eraseSavedObjectPaenls = () => { + return cy + .request({ + method: 'get', + failOnStatusCode: false, + url: 'api/saved_objects/_find?type=observability-panel', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }) + .then((response) => { + response.body.saved_objects.map((soPanel) => { + cy.request({ + method: 'DELETE', + failOnStatusCode: false, + url: `api/saved_objects/observability-panel/${soPanel.id}`, + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }); + }); + }); +}; + +const eraseTestPanels = () => { + eraseLegacyPanels(); + eraseSavedObjectPaenls(); +}; +const uuidRx = /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/; + +const clickCreatePanelButton = () => + cy.get('a[data-test-subj="customPanels__createNewPanels"]').click(); + +const createSavedObjectPanel = (newName = TEST_PANEL) => { + const result = cy + .request({ + method: 'POST', + failOnStatusCode: false, + url: 'api/saved_objects/observability-panel', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + body: { + attributes: { + title: newName, + description: '', + dateCreated: 1681127334085, + dateModified: 1681127334085, + timeRange: { + to: 'now', + from: 'now-1d', + }, + queryFilter: { + query: '', + language: 'ppl', + }, + visualizations: [], + applicationId: '', + }, + }, + }) + .then((response) => console.log(response)); +}; + +const createLegacyPanel = (newName = TEST_PANEL) => { + const result = cy.request({ + method: 'POST', + failOnStatusCode: false, + url: 'api/observability/operational_panels/panels', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + body: { panelName: newName }, + }); +}; + +const expectUuid = (anchorElem) => { + anchorElem.invoke('attr', 'href').should('match', uuidRx); +}; + +const expectLegacyId = (anchorElem) => { + anchorElem.invoke('attr', 'href').should('not.match', uuidRx); +}; + +const clickDeleteAction = () => { + cy.get('button[data-test-subj="deleteContextMenuItem"]').click(); +}; + +const openActionsDropdown = () => { + cy.get('button[data-test-subj="operationalPanelsActionsButton"]').click(); +}; + +const selectThePanel = () => { + cy.get('.euiCheckbox__input[title="Select this row"]').then(() => { + cy.get('.euiCheckbox__input[title="Select this row"]').check({ force: true }); + }); +}; + +const expectToastWith = (title) => { + cy.get('.euiToastHeader__title').contains(title).should('exist'); +}; + +const confirmModal = () => { + cy.get('button[data-test-subj="runModalButton"]').click(); +}; diff --git a/.cypress/integration/7_app_analytics.spec.js b/.cypress/integration/7_app_analytics.spec.js index e3a41a49b8..5de3989719 100644 --- a/.cypress/integration/7_app_analytics.spec.js +++ b/.cypress/integration/7_app_analytics.spec.js @@ -481,7 +481,7 @@ describe('Separate from other plugins', () => { }) }); - it('Hides application visualizations in Operational Panels', () => { + it('Hides application visualizations in Observability Dashboards', () => { cy.visit( `${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/operational_panels/` ); @@ -497,7 +497,7 @@ describe('Separate from other plugins', () => { cy.get('option').contains(visTwoName).should('not.exist'); }); - it('Hides application panels in Operational Panels', () => { + it('Hides application panels in Observability Dashboards', () => { cy.visit( `${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/operational_panels/` ); diff --git a/.cypress/plugins/index.js b/.cypress/plugins/index.js index 8ac1f10667..ddc95c953b 100644 --- a/.cypress/plugins/index.js +++ b/.cypress/plugins/index.js @@ -17,10 +17,14 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) + /** * @type {Cypress.PluginConfig} */ module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + require('cypress-watch-and-reload/plugins')(config) + + return config } diff --git a/.cypress/support/index.js b/.cypress/support/index.js index 6b25b7b27e..2fa692d7a1 100644 --- a/.cypress/support/index.js +++ b/.cypress/support/index.js @@ -18,6 +18,8 @@ // https://on.cypress.io/configuration // *********************************************************** +import 'cypress-watch-and-reload/support' + // Import commands.js using ES2015 syntax: import './commands'; diff --git a/README.md b/README.md index a098f3ffd5..eba1a7f066 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Observability is collection of plugins and applications that let you visualize d ## Plugin Components -The Dashboards Observability plugin has four components: Trace Analytics, Event Analytics, Operational Panels, and Notebooks. +The Dashboards Observability plugin has four components: Trace Analytics, Event Analytics, Observability Dashboards, and Notebooks. ### Trace Analytics @@ -93,9 +93,9 @@ Additionally the fields can be sorted and filtered. Event Analytics allows user to monitor, correlate, analyze and visualize machine generated data through [Piped Processing Language](https://opensearch.org/docs/latest/observability-plugins/ppl/index/). It also enables the user to turn data-driven events into visualizations and save frequently used ones for quick access. -### Operational Panels +### Observability Dashboards -Operational panels provides the users to create and view different visualizations on ingested observability data, using Piped Processing Language queries. Use PPL 'where clauses' and datetime timespans to filter all visualizations in the panel. +Observability Dashboards provides the users to create and view different visualizations on ingested observability data, using Piped Processing Language queries. Use PPL 'where clauses' and datetime timespans to filter all visualizations in the dashboard. ### Notebooks diff --git a/common/constants/custom_panels.ts b/common/constants/custom_panels.ts index 0c02b97a29..555f778b19 100644 --- a/common/constants/custom_panels.ts +++ b/common/constants/custom_panels.ts @@ -3,6 +3,89 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { v4 as uuidv4 } from 'uuid'; + export const CUSTOM_PANELS_API_PREFIX = '/api/observability/operational_panels'; -export const CUSTOM_PANELS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/operational-panels/'; -export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this custom panel.'; +export const CUSTOM_PANELS_DOCUMENTATION_URL = + 'https://opensearch.org/docs/latest/observability-plugin/operational-panels/'; +export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this Observability Dashboard.'; + +export const CUSTOM_PANELS_SAVED_OBJECT_TYPE = 'observability-panel'; + +export const CUSTOM_PANEL_SLICE = 'customPanel'; + +export const samplePanelName = '[Logs] Web traffic Panel'; + +export const createDemoPanel = (savedVisualizationIds: string[]) => { + return { + name: samplePanelName, + visualizations: [ + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[0], + x: 4, + y: 6, + w: 8, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[1], + x: 0, + y: 2, + w: 12, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[2], + x: 0, + y: 0, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[3], + x: 4, + y: 0, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[4], + x: 8, + y: 0, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[5], + x: 0, + y: 4, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[6], + x: 0, + y: 6, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[7], + x: 4, + y: 4, + w: 8, + h: 2, + }, + ], + timeRange: { to: 'now/y', from: 'now/y' }, + queryFilter: { query: '', language: 'ppl' }, + }; +}; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index f1e936e123..e42bd11c60 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -23,9 +23,33 @@ export const PPL_ENDPOINT = '/_plugins/_ppl'; export const SQL_ENDPOINT = '/_plugins/_sql'; export const DSL_ENDPOINT = '/_plugins/_dsl'; -export const observabilityID = 'observability-dashboards'; +export const observabilityID = 'observability-logs'; export const observabilityTitle = 'Observability'; -export const observabilityPluginOrder = 6000; +export const observabilityPluginOrder = 1500; + +export const observabilityApplicationsID = 'observability-applications'; +export const observabilityApplicationsTitle = 'Applications'; +export const observabilityApplicationsPluginOrder = 5090; + +export const observabilityLogsID = 'observability-logs'; +export const observabilityLogsTitle = 'Logs'; +export const observabilityLogsPluginOrder = 5091; + +export const observabilityMetricsID = 'observability-metrics'; +export const observabilityMetricsTitle = 'Metrics'; +export const observabilityMetricsPluginOrder = 5092; + +export const observabilityTracesID = 'observability-traces'; +export const observabilityTracesTitle = 'Traces'; +export const observabilityTracesPluginOrder = 5093; + +export const observabilityNotebookID = 'observability-notebooks'; +export const observabilityNotebookTitle = 'Notebooks'; +export const observabilityNotebookPluginOrder = 5094; + +export const observabilityPanelsID = 'observability-dashboards'; +export const observabilityPanelsTitle = 'Dashboards'; +export const observabilityPanelsPluginOrder = 5095; // Shared Constants export const SQL_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/search-plugins/sql/index/'; diff --git a/common/types/custom_panels.ts b/common/types/custom_panels.ts index ca75e41def..a24cbe839f 100644 --- a/common/types/custom_panels.ts +++ b/common/types/custom_panels.ts @@ -4,11 +4,19 @@ */ export interface CustomPanelListType { - name: string; + title: string; id: string; dateCreated: number; dateModified: number; applicationId?: string; + savedObject: boolean; +} + +export interface BoxType { + x1: number; + y1: number; + x2: number; + y2: number; } export interface VisualizationType { @@ -21,13 +29,19 @@ export interface VisualizationType { } export interface PanelType { - name: string; + title: string; + dateCreated: number; + dateModified: number; visualizations: VisualizationType[]; timeRange: { to: string; from: string }; queryFilter: { query: string; language: string }; applicationId?: string; } +export interface CustomPanelType extends PanelType { + id: string; +} + export interface SavedVisualizationType { id: string; name: string; @@ -39,7 +53,7 @@ export interface SavedVisualizationType { user_configs: any; } -export interface pplResponse { +export interface PPLResponse { data: any; metadata: any; size: number; @@ -50,3 +64,20 @@ export interface VizContainerError { errorMessage: string; errorDetails?: string; } + +export interface ObservabilityPanelAttrs { + title: string; + description: string; + dateCreated: number; + dateModified: number; + timeRange: { + to: string; + from: string; + }; + queryFilter: { + query: string; + language: string; + }; + visualizations: VisualizationType[]; + applicationId: string; +} diff --git a/common/types/explorer.ts b/common/types/explorer.ts index 53dfe2ca20..9c5d5a9439 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -37,6 +37,7 @@ import { SavedObjectAttributes, SavedObjectsStart, } from '../../../../src/core/public/saved_objects'; +import { ChromeBreadcrumb } from '../../../../src/core/public/chrome'; export interface IQueryTab { id: string; @@ -318,7 +319,7 @@ export interface Breadcrumbs { export interface EventAnalyticsProps { chrome: CoreSetup; - parentBreadcrumbs: Breadcrumbs[]; + parentBreadcrumbs: ChromeBreadcrumb[]; pplService: any; dslService: any; savedObjects: SavedObjectsStart; @@ -326,6 +327,7 @@ export interface EventAnalyticsProps { http: HttpStart; notifications: NotificationsStart; queryManager: QueryManager; + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; } export interface DataConfigPanelProps { diff --git a/cypress.json b/cypress.json index dbe41c79b7..07b337dccb 100644 --- a/cypress.json +++ b/cypress.json @@ -1,7 +1,7 @@ { "baseUrl": "http://localhost:5601", "video": true, - "chromeWebSecurity": false, + "chromeWebSecurity": true, "fixturesFolder": ".cypress/fixtures", "integrationFolder": ".cypress/integration", "pluginsFile": ".cypress/plugins/index.js", @@ -19,5 +19,8 @@ "opensearch": "localhost:9200", "opensearchDashboards": "localhost:5601", "security_enabled": true + }, + "cypress-watch-and-reload": { + "watch": ["common/**", "public/**", "server/**"] } } diff --git a/package.json b/package.json index 96c6890796..fc29a1bbf9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/react-test-renderer": "^16.9.1", "antlr4ts-cli": "^0.5.0-alpha.4", "cypress": "^6.0.0", + "cypress-watch-and-reload": "^1.10.6", "eslint": "^6.8.0", "jest-dom": "^4.0.0", "ts-jest": "^29.1.0" diff --git a/public/components/app.tsx b/public/components/app.tsx index 75342b8dd9..18066f3281 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -7,7 +7,6 @@ import { I18nProvider } from '@osd/i18n/react'; import { QueryManager } from 'common/query_manager'; import React from 'react'; import { Provider } from 'react-redux'; -import { HashRouter, Route, Switch } from 'react-router-dom'; import { CoreStart } from '../../../../src/core/public'; import { observabilityID, observabilityTitle } from '../../common/constants/shared'; import { store } from '../framework/redux/store'; @@ -28,6 +27,7 @@ interface ObservabilityAppDeps { savedObjects: any; timestampUtils: any; queryManager: QueryManager; + startPage: string; } // for cypress to test redux store @@ -35,6 +35,15 @@ if (window.Cypress) { window.store = store; } +const pages = { + applications: ApplicationAnalyticsHome, + logs: EventAnalytics, + metrics: MetricsHome, + traces: TraceAnalyticsHome, + notebooks: NotebooksHome, + dashboards: CustomPanelsHome, +}; + export const App = ({ CoreStartProp, DepsStart, @@ -43,128 +52,40 @@ export const App = ({ savedObjects, timestampUtils, queryManager, + startPage, }: ObservabilityAppDeps) => { - const { chrome, http, notifications } = CoreStartProp; + const { chrome, http, notifications, savedObjects: coreSavedObjects } = CoreStartProp; const parentBreadcrumb = { text: observabilityTitle, href: `${observabilityID}#/`, }; - const customPanelBreadcrumb = { - text: 'Operational panels', - href: '#/operational_panels/', - }; + const ModuleComponent = pages[startPage]; return ( - - - - - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { text: 'Metrics analytics', href: '#/metrics_analytics/' }, - ]); - return ( - - ); - }} - /> - { - return ( - - ); - }} - /> - ( - - )} - /> - { - chrome.setBreadcrumbs([parentBreadcrumb, customPanelBreadcrumb]); - return ( - - ); - }} - /> - ( - - )} - /> - { - return ( - - ); - }} - /> - - - - + + + + + ); }; diff --git a/public/components/application_analytics/components/app_table.tsx b/public/components/application_analytics/components/app_table.tsx index 75fe228af8..993cee0fef 100644 --- a/public/components/application_analytics/components/app_table.tsx +++ b/public/components/application_analytics/components/app_table.tsx @@ -37,7 +37,12 @@ import moment from 'moment'; import { DeleteModal } from '../../common/helpers/delete_modal'; import { AppAnalyticsComponentDeps } from '../home'; import { getCustomModal } from '../../custom_panels/helpers/modal_containers'; -import { pageStyles, UI_DATE_FORMAT } from '../../../../common/constants/shared'; +import { + observabilityID, + observabilityTitle, + pageStyles, + UI_DATE_FORMAT, +} from '../../../../common/constants/shared'; import { ApplicationType, AvailabilityType } from '../../../../common/types/application_analytics'; interface AppTableProps extends AppAnalyticsComponentDeps { @@ -72,8 +77,8 @@ export function AppTable(props: AppTableProps) { chrome.setBreadcrumbs([ ...parentBreadcrumbs, { - text: 'Application analytics', - href: '#/application_analytics', + text: 'Applications', + href: '#/', }, ]); clear(); @@ -216,10 +221,7 @@ export function AppTable(props: AppTableProps) { sortable: true, truncateText: true, render: (value, record) => ( - + {_.truncate(record.name, { length: 100 })} ), @@ -284,7 +286,7 @@ export function AppTable(props: AppTableProps) { - + {createButtonText} @@ -324,7 +326,7 @@ export function AppTable(props: AppTableProps) { - + {createButtonText} diff --git a/public/components/application_analytics/components/application.tsx b/public/components/application_analytics/components/application.tsx index b758942583..fa362460f1 100644 --- a/public/components/application_analytics/components/application.tsx +++ b/public/components/application_analytics/components/application.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable no-console */ import { EuiHorizontalRule, @@ -54,7 +53,7 @@ import { } from '../../../../common/constants/application_analytics'; import { TAB_EVENT_ID, TAB_CHART_ID, NEW_TAB } from '../../../../common/constants/explorer'; import { IQueryTab } from '../../../../common/types/explorer'; -import { NotificationsStart } from '../../../../../../src/core/public'; +import { NotificationsStart, Toast } from '../../../../../../src/core/public'; import { AppAnalyticsComponentDeps } from '../home'; import { CustomPanelView } from '../../../../public/components/custom_panels/custom_panel_view'; import { @@ -67,6 +66,7 @@ import { SpanDetailFlyout } from '../../../../public/components/trace_analytics/ import { TraceDetailFlyout } from './flyout_components/trace_detail_flyout'; import { fetchAppById, initializeTabData } from '../helpers/utils'; import { QueryManager } from '../../../../common/query_manager/ppl_query_manager'; +import { observabilityApplicationsID } from '../../../../common/constants/shared'; const searchBarConfigs = { [TAB_EVENT_ID]: { @@ -91,6 +91,7 @@ interface AppDetailProps extends AppAnalyticsComponentDeps { updateApp: (appId: string, updateAppData: Partial, type: string) => void; setToasts: (title: string, color?: string, text?: ReactChild) => void; callback: (childfunction: () => void) => void; + toasts: Toast[]; } export function Application(props: AppDetailProps) { @@ -110,6 +111,7 @@ export function Application(props: AppDetailProps) { updateApp, setAppConfigs, setToasts, + toasts, setFilters, callback, queryManager, @@ -218,12 +220,12 @@ export function Application(props: AppDetailProps) { chrome.setBreadcrumbs([ ...parentBreadcrumbs, { - text: 'Application analytics', - href: '#/application_analytics', + text: 'Applications', + href: '#/', }, { text: application.name, - href: `${last(parentBreadcrumbs)!.href}application_analytics/${appId}`, + href: `${observabilityApplicationsID}/${appId}`, }, ]); setStartTimeForApp(sessionStorage.getItem(`${application.name}StartTime`) || 'now-24h'); @@ -273,12 +275,12 @@ export function Application(props: AppDetailProps) { const childBreadcrumbs = [ { - text: 'Application analytics', - href: '#/application_analytics', + text: 'Applications', + href: '#/', }, { text: `${application.name}`, - href: `#/application_analytics/${appId}`, + href: `#/${appId}`, }, ]; @@ -293,7 +295,9 @@ export function Application(props: AppDetailProps) { endTime={appEndTime} setStartTime={setStartTimeForApp} setEndTime={setEndTimeForApp} + parentBreadcrumb={parentBreadcrumbs[0]} childBreadcrumbs={childBreadcrumbs} + toasts={toasts} /> ); @@ -311,6 +315,7 @@ export function Application(props: AppDetailProps) { page="app" nameColumnAction={nameColumnAction} traceColumnAction={traceColumnAction} + parentBreadcrumb={parentBreadcrumbs[0]} childBreadcrumbs={childBreadcrumbs} startTime={appStartTime} endTime={appEndTime} @@ -334,6 +339,7 @@ export function Application(props: AppDetailProps) { diff --git a/public/components/application_analytics/components/configuration.tsx b/public/components/application_analytics/components/configuration.tsx index 8cb8080ffc..d9f82fbe2a 100644 --- a/public/components/application_analytics/components/configuration.tsx +++ b/public/components/application_analytics/components/configuration.tsx @@ -72,9 +72,7 @@ export const Configuration = (props: ConfigProps) => { fill data-test-subj="editApplicationButton" onClick={() => { - window.location.assign( - `${last(parentBreadcrumbs)!.href}application_analytics/edit/${appId}` - ); + window.location.assign(`#/edit/${appId}`); }} > Edit diff --git a/public/components/application_analytics/components/create.tsx b/public/components/application_analytics/components/create.tsx index 084d53b17d..8bedc3a82b 100644 --- a/public/components/application_analytics/components/create.tsx +++ b/public/components/application_analytics/components/create.tsx @@ -38,6 +38,11 @@ import { OptionType, } from '../../../../common/types/application_analytics'; import { fetchAppById } from '../helpers/utils'; +import { + observabilityApplicationsID, + observabilityID, + observabilityTitle, +} from '../../../../common/constants/shared'; interface CreateAppProps extends AppAnalyticsComponentDeps { dslService: DSLService; @@ -90,12 +95,12 @@ export const CreateApp = (props: CreateAppProps) => { chrome.setBreadcrumbs([ ...parentBreadcrumbs, { - text: 'Application analytics', - href: '#/application_analytics', + text: 'Applications', + href: '#/', }, { text: editMode ? 'Edit' : 'Create', - href: `#/application_analytics/${editMode ? 'edit' : 'create'}`, + href: `#/${editMode ? 'edit' : 'create'}`, }, ]); }, []); @@ -172,7 +177,7 @@ export const CreateApp = (props: CreateAppProps) => { const onCancel = () => { clearStorage(); - window.location.assign(`${last(parentBreadcrumbs)!.href}application_analytics`); + window.location.assign(`${observabilityApplicationsID}#/`); }; return ( diff --git a/public/components/application_analytics/components/flyout_components/service_detail_flyout.tsx b/public/components/application_analytics/components/flyout_components/service_detail_flyout.tsx index f17d46e1df..c21522217c 100644 --- a/public/components/application_analytics/components/flyout_components/service_detail_flyout.tsx +++ b/public/components/application_analytics/components/flyout_components/service_detail_flyout.tsx @@ -19,10 +19,7 @@ import { handleServiceMapRequest, handleServiceViewRequest, } from '../../../../../public/components/trace_analytics/requests/services_request_handler'; -import { - filtersToDsl, - processTimeStamp, -} from '../../../../../public/components/trace_analytics/components/common/helper_functions'; +import { filtersToDsl, processTimeStamp } from '../../../../../public/components/trace_analytics/components/common/helper_functions'; import { ServiceMap } from '../../../../../public/components/trace_analytics/components/services'; import { ServiceObject } from '../../../../../public/components/trace_analytics/components/common/plots/service_map'; import { SpanDetailTable } from '../../../../../public/components/trace_analytics/components/traces/span_detail_table'; diff --git a/public/components/application_analytics/home.tsx b/public/components/application_analytics/home.tsx index 89dec51cfa..1db65a8ac7 100644 --- a/public/components/application_analytics/home.tsx +++ b/public/components/application_analytics/home.tsx @@ -3,10 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable no-console */ import React, { ReactChild, useEffect, useState } from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; 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'; @@ -21,8 +20,7 @@ import { CreateApp } from './components/create'; import { TraceAnalyticsComponentDeps, TraceAnalyticsCoreDeps } from '../trace_analytics/home'; import { FilterType } from '../trace_analytics/components/common/filters/filters'; import { handleDataPrepperIndicesExistRequest } from '../trace_analytics/requests/request_handler'; -import { ObservabilitySideBar } from '../common/side_nav'; -import { NotificationsStart } from '../../../../../src/core/public'; +import { ChromeBreadcrumb, NotificationsStart } from '../../../../../src/core/public'; import { APP_ANALYTICS_API_PREFIX } from '../../../common/constants/application_analytics'; import { ApplicationRequestType, @@ -39,6 +37,7 @@ import { CUSTOM_PANELS_DOCUMENTATION_URL, } from '../../../common/constants/custom_panels'; import { QueryManager } from '../../../common/query_manager/ppl_query_manager'; +import { observabilityApplicationsID } from '../../../common/constants/shared'; export type AppAnalyticsCoreDeps = TraceAnalyticsCoreDeps; @@ -49,6 +48,7 @@ interface HomeProps extends RouteComponentProps, AppAnalyticsCoreDeps { timestampUtils: TimestampUtils; notifications: NotificationsStart; queryManager: QueryManager; + parentBreadcrumbs: ChromeBreadcrumb[]; } export interface AppAnalyticsComponentDeps extends TraceAnalyticsComponentDeps { @@ -59,6 +59,7 @@ export interface AppAnalyticsComponentDeps extends TraceAnalyticsComponentDeps { setQueryWithStorage: (newQuery: string) => void; setFiltersWithStorage: (newFilters: FilterType[]) => void; setAppConfigs: (newAppConfigs: FilterType[]) => void; + parentBreadcrumbs: ChromeBreadcrumb[]; } export const Home = (props: HomeProps) => { @@ -138,7 +139,7 @@ export const Home = (props: HomeProps) => { endTime, setEndTime, mode: 'data_prepper', - dataPrepperIndicesExist: indicesExist + dataPrepperIndicesExist: indicesExist, }; const setToast = (title: string, color = 'success', text?: ReactChild) => { @@ -154,7 +155,7 @@ export const Home = (props: HomeProps) => { }; const moveToApp = (id: string, type: string) => { - window.location.assign(`${last(parentBreadcrumbs)!.href}application_analytics/${id}`); + window.location.assign(`${observabilityApplicationsID}#/${id}`); if (type === 'createSetAvailability') { setTriggerSwitchToEvent(2); } @@ -173,7 +174,7 @@ export const Home = (props: HomeProps) => { }) .catch((err) => { setToast( - 'Please ask your administrator to enable Operational Panels for you.', + 'Please ask your administrator to enable Observability Dashboards for you.', 'danger', Documentation @@ -187,7 +188,7 @@ export const Home = (props: HomeProps) => { const concatList = [appPanelId].toString(); return http.delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList).catch((err) => { setToast( - 'Error occurred while deleting Operational Panels, please make sure you have the correct permission.', + 'Error occurred while deleting Observability Dashboards, please make sure you have the correct permission.', 'danger' ); console.error(err.body.message); @@ -389,12 +390,12 @@ export const Home = (props: HomeProps) => { }} toastLifeTimeMs={6000} /> - - ( - + + + ( { moveToApp={moveToApp} {...commonProps} /> - - )} - /> - ( - - )} - /> - ( - - )} - /> - + )} + /> + ( + + )} + /> + ( + + )} + /> + + ); }; diff --git a/public/components/common/side_nav.tsx b/public/components/common/side_nav.tsx deleted file mode 100644 index cf6f467146..0000000000 --- a/public/components/common/side_nav.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiPage, - EuiPageBody, - EuiPageSideBar, - EuiSideNav, - EuiSideNavItemType, - EuiSwitch, -} from '@elastic/eui'; -import React from 'react'; -import { useState } from 'react'; -import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; -import { uiSettingsService } from '../../../common/utils'; - -export function ObservabilitySideBar(props: { children: React.ReactNode }) { - // set items.isSelected based on location.hash passed in - // tries to find an item where href is a prefix of the hash - // if none will try to find an item where the hash is a prefix of href - function setIsSelected( - items: EuiSideNavItemType[], - hash: string, - initial = true, - reverse = false - ): boolean { - // Default page is Events Analytics - // But it is kept as second option in side nav - if (hash === '#/') { - items[0].items[2].isSelected = true; - return true; - } - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.href && ((reverse && item.href.startsWith(hash)) || hash.startsWith(item.href))) { - item.isSelected = true; - return true; - } - if (item.items?.length && setIsSelected(item.items, hash, false, reverse)) return true; - } - return initial && setIsSelected(items, hash, false, !reverse); - } - - const items = [ - { - name: 'Observability', - id: 0, - items: [ - { - name: 'Application analytics', - id: 1, - href: '#/application_analytics', - }, - { - name: 'Trace analytics', - id: 2, - href: '#/trace_analytics/home', - items: [ - { - name: 'Traces', - id: 2.1, - href: '#/trace_analytics/traces', - }, - { - name: 'Services', - id: 2.2, - href: '#/trace_analytics/services', - }, - ], - }, - { - name: 'Event analytics', - id: 3, - href: '#/event_analytics', - }, - { - name: 'Metrics analytics', - id: 4, - href: '#/metrics_analytics/', - }, - { - name: 'Operational panels', - id: 5, - href: '#/operational_panels/', - }, - { - name: 'Notebooks', - id: 6, - href: '#/notebooks', - }, - ], - }, - ]; - setIsSelected(items, location.hash); - const [isDarkMode, setIsDarkMode] = useState(uiSettingsService.get('theme:darkMode')); - - return ( - - - - - - - - { - uiSettingsService.set('theme:darkMode', !isDarkMode).then((resp) => { - setIsDarkMode(!isDarkMode); - uiSettingsService.addToast({ - title: 'Theme setting changes require you to reload the page to take effect.', - text: toMountPoint( - <> - - - window.location.reload()}> - Reload page - - - - - ), - color: 'success', - }); - }); - }} - /> - - - - {props.children} - - ); -} diff --git a/public/components/common/toast/index.tsx b/public/components/common/toast/index.tsx new file mode 100644 index 0000000000..6eaef004ce --- /dev/null +++ b/public/components/common/toast/index.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ToastInputFields } from '../../../../../../src/core/public'; +import { coreRefs } from '../../../framework/core_refs'; + +type Color = 'success' | 'primary' | 'warning' | 'danger' | undefined; + +export const useToast = () => { + const toasts = coreRefs.toasts!; + + const setToast = (title: string, color: Color = 'success', text?: string) => { + const newToast: ToastInputFields = { + id: new Date().toISOString(), + title, + text, + }; + switch (color) { + case 'danger': { + toasts.addDanger(newToast); + break; + } + case 'warning': { + toasts.addWarning(newToast); + break; + } + default: { + toasts.addSuccess(newToast); + break; + } + } + }; + + return { setToast }; +}; diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap index 337b013063..5ca0ba1254 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap @@ -1,2861 +1,1225 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Panels Table Component renders empty panel table container 1`] = ` - + -
- -
- + +
-
- -
- -
- + +
+ +
-

- Operational panels -

- -
-
-
-
- - + Observability dashboards + +
+
+
+
+
+ -
- -
- -
- + +
+ +
-

- Panels - - ( - 0 - ) - -

- - -
- - -
+ ( + 0 + ) + + + + +
+ + - -
- Use Operational panels to create and view different visualizations on ingested observability data, using PPL (Piped Processing Language) queries. - - - - Learn more - - - - - - - -
-
-
- -
- - -
- + + + + + +
+ +
+ +
+
+ +
-
- -
- - Actions - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" +
+ +
-
-
+ - + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ - - - + + + + +
-
- -
- - -
- +
+
+ + - -
- -
- -
- - -
-
- -
- - -
+ + + +
+ +
+ +
+ +
+ + +
+
+ - + + +
-
-

- No Operational Panels -

- -
- - -
+ No Observability Dashboards + + +
+ + - -
- Use operational panels to dive deeper into observability -
- using PPL queries and insightful visualizations -
-
-
- -
- -
- - -
- - -
+ Use Observability Dashboards to dive deeper into observability +
+ using PPL queries and insightful visualizations +
+ +
+ +
+ +
+
+ +
+ + - - - - -
+ + + +
+
+ - - - - - -
- -
- - -
- -
- - -
- -
- -
- + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ `; exports[`Panels Table Component renders panel table container 1`] = ` - + -
- -
- + +
-
- -
- -
- + +
+ +
-

- Operational panels -

- -
-
-
-
- - + Observability dashboards + +
+
+
+
+
+ -
- -
- -
- + +
+ +
-

- Panels - - ( - 4 - ) - -

- - -
- - -
+ ( + 0 + ) + + + + +
+ + - -
- Use Operational panels to create and view different visualizations on ingested observability data, using PPL (Piped Processing Language) queries. - - - - Learn more - - - - - - - - - -
-
-
- -
- - -
- + + + + + + +
+ +
+ +
+
+ +
-
- -
- - Actions - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" +
+ +
-
-
+ - + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ - - - + + + + +
-
- -
- - -
- +
+
+ + - -
- -
- -
- - -
-
- - + + + +
+ +
+ +
+ +
+ + +
+
+
+ + +
-
- - - - -
+ No Observability Dashboards + + +
+ + - - - - - -
- -
+ Use Observability Dashboards to dive deeper into observability +
+ using PPL queries and insightful visualizations +
+ +
+
+
+
- - - -
-
- - + +
+ +
-
- -
- -
- -
- - -
- -
- -
- - -
- - -
- -
- - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
-
-
-
- -
- - +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - -
- -
-
- - -
- -
-
- - -
-
- - - - - -
-
- - -
- -
-
- - -
-
-
- Name -
- -
-
- Last updated -
-
- 11/04/2021 06:10 PM -
-
-
- Created -
-
- 11/02/2021 07:26 PM -
-
-
- - -
- -
-
- - -
-
-
- Name -
- -
-
- Last updated -
-
- 11/03/2021 10:51 PM -
-
-
- Created -
-
- 11/02/2021 09:44 PM -
-
-
- - -
- -
-
- - -
-
-
- Name -
- -
-
- Last updated -
-
- 11/03/2021 10:37 PM -
-
-
- Created -
-
- 10/29/2021 10:15 PM -
-
-
- - -
- -
-
- - -
-
-
- Name -
- -
-
- Last updated -
-
- 11/03/2021 09:26 PM -
-
-
- Created -
-
- 11/03/2021 09:26 PM -
-
- -
- + + + + + +
+ + -
- -
- - + - -
- -
- - - : - 10 - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
-
- -
- - - -
-
-
-
-
+ Add samples + + + + + +
- +
- - -
- - -
- -
- -
- + + +
+ +
+ +
+
+ +
+
+
+ + `; diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap index 7b2eda6f26..98c0168fd5 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap @@ -1,697 +1,581 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Panels View Component renders panel view container with visualizations 1`] = ` - + -
- -
+
+ - -
- -
- -
- + +
+ +
-

- - -
- + + +
-
- -
- - Created on - Invalid date -
- - -
- +
+ +
+ + Created on + Invalid date +
+
+ +
-
- -
- + +
- - - - -
-
- -
- - Panel actions - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - withTitle={true} - > -
-
- - - - - -
-
-
-
-
- -
+ + + + + Edit + + + + + + +
+
+ - - Add visualization + Dashboard Actions
} closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="addVisualizationContextMenu" isOpen={false} ownFocus={true} panelPaddingSize="none" + withTitle={true} >
- -
-
-
- -
-
-

-
- -
- -
- -
- - PPL - - } - baseQuery="source = " - dslService={ - DSLService { - "fetch": [Function], - "fetchFields": [Function], - "fetchIndices": [Function], - "http": [MockFunction], - } - } - getSuggestions={[Function]} - handleQueryChange={[Function]} - handleQuerySearch={[Function]} - isDisabled={true} - key="autocomplete-search-bar" - onItemSelect={[Function]} - placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" - possibleCommands={ - Array [ - Object { - "label": "where", - }, - ] - } - query="" - tabId="panels-filter" - tempQuery="" - > -
+ + - - PPL - - } - aria-autocomplete="both" - aria-labelledby="autocomplete-4-label" - autoCapitalize="off" - autoComplete="off" - autoCorrect="off" - autoFocus={false} - data-test-subj="searchAutocompleteTextArea" - disabled={true} - enterKeyHint="search" - fullWidth={true} - id="autocomplete-textarea" - maxLength={512} - onBlur={[Function]} - onChange={[Function]} - onClick={[Function]} - onFocus={[Function]} - onKeyDown={[Function]} - placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" - spellCheck="false" - type="search" - value="" +
- - PPL - - } - fullWidth={true} - inputId="autocomplete-textarea" + -
+ Add visualization + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" >
- - - - -
- - - -
-
- -
- -
- - -
- + + + + +
+
+ + +
+ +
+ +
+
+
+
+ +
+ +
+ +
- + PPL + + } + baseQuery="source = " + dslService={ + DSLService { + "fetch": [Function], + "fetchFields": [Function], + "fetchIndices": [Function], + "http": [MockFunction], + } + } + getSuggestions={[Function]} + handleQueryChange={[Function]} + handleQuerySearch={[Function]} + isDisabled={true} + key="autocomplete-search-bar" + onItemSelect={[Function]} + placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" + possibleCommands={ + Array [ + Object { + "label": "where", + }, + ] + } + query="" + tabId="panels-filter" + tempQuery="" >
- -
- + + PPL + + } + aria-autocomplete="both" + aria-labelledby="autocomplete-4-label" + autoCapitalize="off" + autoComplete="off" + autoCorrect="off" + autoFocus={false} + data-test-subj="searchAutocompleteTextArea" + disabled={true} + enterKeyHint="search" + fullWidth={true} + id="autocomplete-textarea" + maxLength={512} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" + spellCheck="false" + type="search" + value="" + > + + PPL + + } + fullWidth={true} + inputId="autocomplete-textarea" + > +
- + + + +
+ + + +
+
+
+
+ +
+ + +
+ + +
+ +
+ + } + > +
+ -
-
+ - - + + + +
-
- - -
- } - iconType={false} - isCustom={true} - startDateControl={
} + + +
-
} + iconType={false} + isCustom={true} + startDateControl={
} > - -
- - + + Show dates + + + +
+ + +
-
-
-
-
- -
+
+
+ - - - - - - - - - - - -
- -
-
- -
-
-
-
- -
- - -
- -
- - -
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ + - -
-

- Start by adding your first visualization -

- -
- - -
+ Start by adding your first visualization + + +
+ + - -
- Use PPL Queries to fetch & filter observability data and create visualizations -
-
-
- -
- -
- - -
- - -
+ Use PPL Queries to fetch & filter observability data and create visualizations +
+ +
+ +
+
+
+ + - + + +
-
- - - Add visualization - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="addVisualizationContextMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" + -
-
+ - + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ - - - + + + + +
-
- - -
- -
- - -
- -
- - + +
+ +
+ + +
+ +
+
+ - - - -
- - - - -
- -
- -
- -
- + maxRows={Infinity} + onDrag={[Function]} + onDragStart={[Function]} + onDragStop={[Function]} + onLayoutChange={[Function]} + onResize={[Function]} + onResizeStart={[Function]} + onResizeStop={[Function]} + preventCollision={false} + rowHeight={150} + style={Object {}} + useCSSTransforms={true} + verticalCompact={true} + width={0} + > +
+ + + + +
+ +
+ +
+
+
+
+ `; exports[`Panels View Component renders panel view container without visualizations 1`] = ` - + -
- -
+
+ - -
- -
- -
- + +
+ +
-

- - -
- + + +
-
- -
- - Created on - Invalid date -
- - -
- +
+ +
+ + Created on + Invalid date +
+
+ +
-
- -
- + +
- - - - -
-
- -
- - Panel actions - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - withTitle={true} - > -
-
- - - - - -
-
-
-
-
- -
+ + + + Edit + + + + + + +
+
+ - - Add visualization + Dashboard Actions
} closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="addVisualizationContextMenu" isOpen={false} ownFocus={true} panelPaddingSize="none" + withTitle={true} >
+ + +
+
+ +
+
+ +
+ + + Add visualization + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + - - + + + + +
-
- - -
- -
- -
- -

-
- -
- + +
+ +
+ +
+ + + + +
-
- -
- + +
+ + PPL + + } + baseQuery="source = " + dslService={ + DSLService { + "fetch": [Function], + "fetchFields": [Function], + "fetchIndices": [Function], + "http": [MockFunction], } - > - PPL - - } - baseQuery="source = " - dslService={ - DSLService { - "fetch": [Function], - "fetchFields": [Function], - "fetchIndices": [Function], - "http": [MockFunction], } - } - getSuggestions={[Function]} - handleQueryChange={[Function]} - handleQuerySearch={[Function]} - isDisabled={true} - key="autocomplete-search-bar" - onItemSelect={[Function]} - placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" - possibleCommands={ - Array [ - Object { - "label": "where", - }, - ] - } - query="" - tabId="panels-filter" - tempQuery="" - > -
- - PPL - - } - aria-autocomplete="both" +
- } + aria-autocomplete="both" + aria-labelledby="autocomplete-1-label" + autoCapitalize="off" + autoComplete="off" + autoCorrect="off" + autoFocus={false} + data-test-subj="searchAutocompleteTextArea" + disabled={true} + enterKeyHint="search" fullWidth={true} - inputId="autocomplete-textarea" + id="autocomplete-textarea" + maxLength={512} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" + spellCheck="false" + type="search" + value="" > -
+ PPL + + } + fullWidth={true} + inputId="autocomplete-textarea" >
- - - - -
- -
+ - PPL - - -
- -
-
-
-
-
- -
+ PPL + + +
+ + +
+ +
+ + - - -
- -
- - } +
+ +
-
+ } > - - - - + -
-
+ - - + + + +
-
- - -
- } - iconType={false} - isCustom={true} - startDateControl={
} + + +
-
} + iconType={false} + isCustom={true} + startDateControl={
} > - -
- - + + Show dates + + + +
+ + +
-
- -
-
- -
+
+
+ - - - - - - - - - - - -
- -
- - -
-
-
- - -
- - -
- -
- - -
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ + - -
-

- Start by adding your first visualization -

- -
- - -
+ Start by adding your first visualization + + +
+ + - -
- Use PPL Queries to fetch & filter observability data and create visualizations -
-
-
- -
- -
- - -
- - -
+ Use PPL Queries to fetch & filter observability data and create visualizations +
+ +
+ +
+
+
+ + +
+ + - -
- - - Add visualization - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="addVisualizationContextMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" + -
-
+ - + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ - - - + + + + +
-
- - -
- -
-
- -
- -
- - + +
+ +
+ + +
+ +
+
+ - - - -
- - - - -
- -
-
-
- -
- + maxRows={Infinity} + onDrag={[Function]} + onDragStart={[Function]} + onDragStop={[Function]} + onLayoutChange={[Function]} + onResize={[Function]} + onResizeStart={[Function]} + onResizeStop={[Function]} + preventCollision={false} + rowHeight={150} + style={Object {}} + useCSSTransforms={true} + verticalCompact={true} + width={0} + > +
+ + + + +
+ +
+ +
+ + +
+ `; diff --git a/public/components/custom_panels/__tests__/custom_panel_table.test.tsx b/public/components/custom_panels/__tests__/custom_panel_table.test.tsx index 811bbf9c84..b3bcd71af8 100644 --- a/public/components/custom_panels/__tests__/custom_panel_table.test.tsx +++ b/public/components/custom_panels/__tests__/custom_panel_table.test.tsx @@ -3,13 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { applyMiddleware, createStore } from '@reduxjs/toolkit'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; -import { CustomPanelTable } from '../custom_panel_table'; -import { waitFor, render, screen, fireEvent } from '@testing-library/react'; -import { panelBreadCrumbs, panelsData } from '../../../../test/panels_constants'; +import { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; import { CustomPanelListType } from '../../../../common/types/custom_panels'; +import { panelBreadCrumbs, panelsData } from '../../../../test/panels_constants'; +import { coreRefs } from '../../../framework/core_refs'; +import { rootReducer } from '../../../framework/redux/reducers'; +import { CustomPanelTable } from '../custom_panel_table'; jest.mock('react-router-dom', () => ({ useLocation: jest.fn().mockReturnValue({ @@ -19,11 +24,18 @@ jest.mock('react-router-dom', () => ({ state: null, key: '', }), - useHistory: jest.fn() + useHistory: jest.fn(), })); describe('Panels Table Component', () => { configure({ adapter: new Adapter() }); + const store = createStore(rootReducer, applyMiddleware(thunk)); + coreRefs.savedObjectsClient.find = jest.fn(() => + Promise.resolve({ + savedObjects: [], + then: () => Promise.resolve(), + }) + ); it('renders empty panel table container', async () => { const loading = false; @@ -38,18 +50,20 @@ describe('Panels Table Component', () => { const addSamplePanels = jest.fn(); const wrapper = mount( - + + + ); wrapper.update(); @@ -71,18 +85,20 @@ describe('Panels Table Component', () => { const addSamplePanels = jest.fn(); const wrapper = mount( - + + + ); wrapper.update(); @@ -102,24 +118,26 @@ describe('Panels Table Component', () => { const cloneCustomPanel = jest.fn(); const deleteCustomPanelList = jest.fn(); const addSamplePanels = jest.fn(); - + const utils = render( - + + + ); - fireEvent.click(screen.getAllByText('Create panel')[0]); + fireEvent.click(screen.getAllByText('Create Dashboard')[0]); await waitFor(() => { - expect(global.window.location.href).toContain('/create') + expect(global.window.location.href).toContain('/create'); }); }); }); diff --git a/public/components/custom_panels/__tests__/custom_panel_view.test.tsx b/public/components/custom_panels/__tests__/custom_panel_view.test.tsx index 81cb151cd6..d4eb276e66 100644 --- a/public/components/custom_panels/__tests__/custom_panel_view.test.tsx +++ b/public/components/custom_panels/__tests__/custom_panel_view.test.tsx @@ -20,10 +20,16 @@ import PPLService from '../../../../public/services/requests/ppl'; import DSLService from '../../../../public/services/requests/dsl'; import { coreStartMock } from '../../../../test/__mocks__/coreMocks'; import { HttpResponse } from '../../../../../../src/core/public'; +import { applyMiddleware, createStore } from 'redux'; +import { rootReducer } from '../../../framework/redux/reducers'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; describe('Panels View Component', () => { configure({ adapter: new Adapter() }); + const store = createStore(rootReducer, applyMiddleware(thunk)); + it('renders panel view container without visualizations', async () => { httpClientMock.get = jest.fn(() => Promise.resolve((sampleEmptyPanel as unknown) as HttpResponse) @@ -47,24 +53,26 @@ describe('Panels View Component', () => { }; const wrapper = mount( - + + + ); wrapper.update(); @@ -106,24 +114,26 @@ describe('Panels View Component', () => { }; const wrapper = mount( - + + + ); wrapper.update(); diff --git a/public/components/custom_panels/custom_panel_table.tsx b/public/components/custom_panels/custom_panel_table.tsx index 50054523ad..f2435226f8 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/public/components/custom_panels/custom_panel_table.tsx @@ -32,18 +32,33 @@ import { import React, { ReactElement, useEffect, useState } from 'react'; import moment from 'moment'; import _ from 'lodash'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { coreRefs } from '../../framework/core_refs'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE, + CUSTOM_PANELS_API_PREFIX, CUSTOM_PANELS_DOCUMENTATION_URL, } from '../../../common/constants/custom_panels'; import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { getCustomModal } from './helpers/modal_containers'; -import { CustomPanelListType } from '../../../common/types/custom_panels'; +import { CustomPanelListType, CustomPanelType } from '../../../common/types/custom_panels'; import { getSampleDataModal } from '../common/helpers/add_sample_modal'; import { pageStyles } from '../../../common/constants/shared'; import { DeleteModal } from '../common/helpers/delete_modal'; -import { useHistory, useLocation } from 'react-router-dom'; +import { + createPanel, + deletePanels, + doesNameExist, + fetchPanels, + isUuid, + newPanelTemplate, + renameCustomPanel, + selectPanelList, +} from './redux/panel_slice'; +import { isNameValid } from './helpers/utils'; +import { useToast } from '../common/toast'; /* * "CustomPanelTable" module, used to view all the saved panels @@ -62,46 +77,38 @@ import { useHistory, useLocation } from 'react-router-dom'; interface Props { loading: boolean; - fetchCustomPanels: () => void; - customPanels: CustomPanelListType[]; - createCustomPanel: (newCustomPanelName: string) => void; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; parentBreadcrumbs: EuiBreadcrumb[]; - renameCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; - cloneCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; - deleteCustomPanelList: (customPanelIdList: string[], toastMessage: string) => any; addSamplePanels: () => void; } export const CustomPanelTable = ({ loading, - fetchCustomPanels, - customPanels, - createCustomPanel, setBreadcrumbs, parentBreadcrumbs, - renameCustomPanel, - cloneCustomPanel, - deleteCustomPanelList, addSamplePanels, }: Props) => { + const customPanels = useSelector(selectPanelList); const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle const [modalLayout, setModalLayout] = useState(); // Modal Layout const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); - const [selectedCustomPanels, setselectedCustomPanels] = useState([]); + const [selectedCustomPanels, setselectedCustomPanels] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const location = useLocation(); const history = useHistory(); + const dispatch = useDispatch(); + const { setToast } = useToast(); + useEffect(() => { setBreadcrumbs(parentBreadcrumbs); - fetchCustomPanels(); + dispatch(fetchPanels()); }, []); useEffect(() => { - const url = window.location.hash.split('/') - if (url[url.length-1] === 'create') { - createPanel(); + const url = window.location.hash.split('/'); + if (url[url.length - 1] === 'create') { + createPanelModal(); } }, [location]); @@ -114,39 +121,77 @@ export const CustomPanelTable = ({ }; const onCreate = async (newCustomPanelName: string) => { - createCustomPanel(newCustomPanelName); + if (await doesNameExist(newCustomPanelName)) { + setToast(`Observability Dashboard with name "${newCustomPanelName}" already exists`, 'danger'); + } else if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + const newPanel = newPanelTemplate(newCustomPanelName); + dispatch(createPanel(newPanel)); + } closeModal(); }; const onRename = async (newCustomPanelName: string) => { - renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id); + if (await doesNameExist(newCustomPanelName)) { + setToast(`Observability Dashboard with name "${newCustomPanelName}" already exists`, 'danger'); + } else if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + dispatch(renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id)); + } closeModal(); }; const onClone = async (newName: string) => { - cloneCustomPanel(newName, selectedCustomPanels[0].id); + if (await doesNameExist(newName)) { + setToast(`Observability Dashboard with name "${newName}" already exists`, 'danger'); + } else if (!isNameValid(newName)) { + setToast('Invalid Observability Dashboard name', 'danger'); + } else { + let sourcePanel = selectedCustomPanels[0]; + try { + if (!isUuid(sourcePanel.id)) { + // Observability Panel API returns partial record, so for duplication + // we will retrieve the entire record and allow new process to continue. + const legacyFetchResult = await coreRefs.http!.get( + `${CUSTOM_PANELS_API_PREFIX}/panels/${sourcePanel.id}` + ); + sourcePanel = legacyFetchResult.operationalPanel; + } + + const { id, ...newPanel } = { + ...sourcePanel, + title: newName, + }; + + dispatch(createPanel(newPanel)); + } catch (err) { + setToast( + 'Error cloning Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(err); + } + } closeModal(); }; const onDelete = async () => { - const toastMessage = `Custom Panels ${ - selectedCustomPanels.length > 1 ? 's' : ' ' + selectedCustomPanels[0].name - } successfully deleted!`; - const PanelList = selectedCustomPanels.map((panel) => panel.id); - deleteCustomPanelList(PanelList, toastMessage); + dispatch(deletePanels(selectedCustomPanels)); closeModal(); }; - const createPanel = () => { + const createPanelModal = () => { setModalLayout( getCustomModal( onCreate, () => { - closeModal() + closeModal(); history.goBack(); }, 'Name', - 'Create operational panel', + 'Create Observability Dashboard', 'Cancel', 'Create', undefined, @@ -162,26 +207,26 @@ export const CustomPanelTable = ({ onRename, closeModal, 'Name', - 'Rename Panel', + 'Rename Dashboard', 'Cancel', 'Rename', - selectedCustomPanels[0].name, + selectedCustomPanels[0].title, CREATE_PANEL_MESSAGE ) ); showModal(); }; - const clonePanel = () => { + const clonePanelModal = () => { setModalLayout( getCustomModal( onClone, closeModal, 'Name', - 'Duplicate Panel', + 'Duplicate Dashboard', 'Cancel', 'Duplicate', - selectedCustomPanels[0].name + ' (copy)', + selectedCustomPanels[0].title + ' (copy)', CREATE_PANEL_MESSAGE ) ); @@ -189,7 +234,9 @@ export const CustomPanelTable = ({ }; const deletePanel = () => { - const customPanelString = `operational panel${selectedCustomPanels.length > 1 ? 's' : ''}`; + const customPanelString = `Observability Dashboard${ + selectedCustomPanels.length > 1 ? 's' : '' + }`; setModalLayout( ); - const popoverItems: ReactElement[] = [ + const popoverItems = (): ReactElement[] => [ { setIsActionsPopoverOpen(false); - clonePanel(); + clonePanelModal(); }} > Duplicate @@ -270,7 +317,7 @@ export const CustomPanelTable = ({ const tableColumns = [ { - field: 'name', + field: 'title', name: 'Name', sortable: true, truncateText: true, @@ -301,7 +348,7 @@ export const CustomPanelTable = ({ -

Operational panels

+

Observability dashboards

@@ -310,14 +357,14 @@ export const CustomPanelTable = ({

- Panels + Dashboard ({customPanels.length})

- Use Operational panels to create and view different visualizations on ingested - observability data, using PPL (Piped Processing Language) queries.{' '} + Use Observability Dashboard to create and view different visualizations on + ingested observability data, using PPL (Piped Processing Language) queries.{' '} Learn more @@ -332,16 +379,12 @@ export const CustomPanelTable = ({ isOpen={isActionsPopoverOpen} closePopover={() => setIsActionsPopoverOpen(false)} > - + - - Create panel + + Create Dashboard
@@ -353,7 +396,7 @@ export const CustomPanelTable = ({ setSearchQuery(e.target.value)} /> @@ -363,7 +406,7 @@ export const CustomPanelTable = ({ items={ searchQuery ? customPanels.filter((customPanel) => - customPanel.name.toLowerCase().includes(searchQuery.toLowerCase()) + customPanel.title.toLowerCase().includes(searchQuery.toLowerCase()) ) : customPanels } @@ -391,10 +434,10 @@ export const CustomPanelTable = ({ <> -

No Operational Panels

+

No Observability Dashboards

- Use operational panels to dive deeper into observability + Use Observability Dashboards to dive deeper into observability
using PPL queries and insightful visualizations
@@ -405,9 +448,9 @@ export const CustomPanelTable = ({ - Create panel + Create Dashboard diff --git a/public/components/custom_panels/custom_panel_view.tsx b/public/components/custom_panels/custom_panel_view.tsx index 41766d4bd8..0b49b039d7 100644 --- a/public/components/custom_panels/custom_panel_view.tsx +++ b/public/components/custom_panels/custom_panel_view.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ /* eslint-disable react-hooks/exhaustive-deps */ import { @@ -30,14 +29,18 @@ import { last } from 'lodash'; import React, { useEffect, useState } from 'react'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import moment from 'moment'; +import _ from 'lodash'; +import { useDispatch } from 'react-redux'; import DSLService from '../../services/requests/dsl'; import { CoreStart } from '../../../../../src/core/public'; import { EmptyPanelView } from './panel_modules/empty_panel'; import { CREATE_PANEL_MESSAGE, CUSTOM_PANELS_API_PREFIX, + CUSTOM_PANELS_SAVED_OBJECT_TYPE, } from '../../../common/constants/custom_panels'; import { + PanelType, SavedVisualizationType, VisualizationType, VizContainerError, @@ -51,6 +54,7 @@ import { onTimeChange, isPPLFilterValid, fetchVisualizationById, + isNameValid, } from './helpers/utils'; import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { VisaulizationFlyout } from './panel_modules/visualization_flyout'; @@ -64,10 +68,12 @@ import { } from '../common/search/autocomplete_logic'; import { AddVisualizationPopover } from './helpers/add_visualization_popover'; import { DeleteModal } from '../common/helpers/delete_modal'; -import _ from 'lodash'; +import { coreRefs } from '../../framework/core_refs'; +import { clonePanel } from './redux/panel_slice'; +import { useToast } from '../common/toast'; /* - * "CustomPanelsView" module used to render an Operational Panel + * "CustomPanelsView" module used to render an Observability Dashboard * * Props taken in as params are: * panelId: Name of the panel opened @@ -77,7 +83,6 @@ import _ from 'lodash'; * dslService: dsl requestor service * chrome: chrome core service * parentBreadcrumb: parent breadcrumb - * renameCustomPanel: Rename function for the panel * deleteCustomPanel: Delete function for the panel * cloneCustomPanel: Clone function for the panel * setToast: create Toast function @@ -99,15 +104,7 @@ interface CustomPanelViewProps { dslService: DSLService; chrome: CoreStart['chrome']; parentBreadcrumbs: EuiBreadcrumb[]; - renameCustomPanel: (editedCustomPanelName: string, editedCustomPanelId: string) => Promise; - deleteCustomPanel: (customPanelId: string, customPanelName: string) => Promise; cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; - setToast: ( - title: string, - color?: string, - text?: React.ReactChild | undefined, - side?: string | undefined - ) => void; onEditClick: (savedVisualizationId: string) => any; startTime: string; endTime: string; @@ -135,13 +132,12 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { setStartTime, setEndTime, updateAvailabilityVizId, - renameCustomPanel, - deleteCustomPanel, cloneCustomPanel, - setToast, onEditClick, onAddClick, } = props; + + const [panel, setPanel] = useState(); const [openPanelName, setOpenPanelName] = useState(''); const [panelCreatedTime, setPanelCreatedTime] = useState(''); const [pplFilterValue, setPPLFilterValue] = useState(''); @@ -165,6 +161,10 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { const appPanel = page === 'app'; + const dispatch = useDispatch(); + + const { setToast } = useToast(); + const closeHelpFlyout = () => { setAddVizDisabled(false); setHelpIsFlyoutVisible(false); @@ -183,6 +183,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { return http .get(`${CUSTOM_PANELS_API_PREFIX}/panels/${panelId}`) .then((res) => { + setPanel(res.operationalPanel); setOpenPanelName(res.operationalPanel.name); setPanelCreatedTime(res.createdTimeMs); setPPLFilterValue(res.operationalPanel.queryFilter.query); @@ -191,7 +192,60 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { setPanelVisualizations(res.operationalPanel.visualizations); }) .catch((err) => { - console.error('Issue in fetching the operational panels', err); + console.error('Issue in fetching the Observability Dashboards', err); + }); + }; + + // Renames an existing CustomPanel + const renameCustomPanel = (editedCustomPanelName: string, editedCustomPanelId: string) => { + if (!isNameValid(editedCustomPanelName)) { + setToast('Invalid Custom Panel name', 'danger'); + return Promise.reject(); + } + const renamePanelObject = { + panelId: editedCustomPanelId, + panelName: editedCustomPanelName, + }; + + return http + .post(`${CUSTOM_PANELS_API_PREFIX}/panels/rename`, { + body: JSON.stringify(renamePanelObject), + }) + .then((res) => { + setOpenPanelName(editedCustomPanelName); + // setOpenPanelName((prevCustomPanelData) => { + // const newCustomPanelData = [...prevCustomPanelData]; + // const renamedCustomPanel = newCustomPanelData.find( + // (customPanel) => customPanel.id === editedCustomPanelId + // ); + // if (renamedCustomPanel) renamedCustomPanel.name = editedCustomPanelName; + // return newCustomPanelData; + // }); + setToast(`Observability Dashboard successfully renamed into "${editedCustomPanelName}"`); + }) + .catch((err) => { + setToast( + 'Error renaming Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(err.body.message); + }); + }; + + // Deletes an existing Observability Dashboard + const deleteCustomPanel = (customPanelId: string, customPanelName: string) => { + return coreRefs + .http!.delete(`${CUSTOM_PANELS_API_PREFIX}/panels/` + customPanelId) + .then((res) => { + setToast(`Observability Dashboard "${customPanelName}" successfully deleted!`); + return res; + }) + .catch((err) => { + setToast( + 'Error deleting Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(err.body.message); }); }; @@ -208,14 +262,10 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }; const onDatePickerChange = (timeProps: OnTimeChangeProps) => { - onTimeChange( - timeProps.start, - timeProps.end, - recentlyUsedRanges, - setRecentlyUsedRanges, - setStartTime, - setEndTime - ); + const { updatedRanges } = onTimeChange(timeProps.start, timeProps.end, recentlyUsedRanges); + setStartTime(timeProps.start); + setEndTime(timeProps.end); + setRecentlyUsedRanges(updatedRanges); onRefreshFilters(timeProps.start, timeProps.end); }; @@ -234,7 +284,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onConfirm={onDelete} onCancel={closeModal} title={`Delete ${openPanelName}`} - message={`Are you sure you want to delete this Operational Panel?`} + message={`Are you sure you want to delete this Observability Dashboard?`} /> ); showModal(); @@ -253,7 +303,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onRename, closeModal, 'Name', - 'Rename Panel', + 'Rename Dashboard', 'Cancel', 'Rename', openPanelName, @@ -264,19 +314,21 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }; const onClone = async (newCustomPanelName: string) => { - cloneCustomPanel(newCustomPanelName, panelId).then((id: string) => { - window.location.assign(`${last(parentBreadcrumbs)!.href}${id}`); - }); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Observability Dashboard name', 'danger'); + } else { + dispatch(clonePanel(panel, newCustomPanelName)); + } closeModal(); }; - const clonePanel = () => { + const clonePanelModal = () => { setModalLayout( getCustomModal( onClone, closeModal, 'Name', - 'Duplicate Panel', + 'Duplicate Dashboard', 'Cancel', 'Duplicate', openPanelName + ' (copy)', @@ -382,7 +434,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { setOnRefresh(!onRefresh); }) .catch((err) => { - setToast('Error is adding filters to the operational panel', 'danger'); + setToast('Error is adding filters to the Observability Dashboard', 'danger'); console.error(err.body.message); }); }; @@ -400,7 +452,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { setToast(`Visualization ${visualzationTitle} successfully added!`, 'success'); }) .catch((err) => { - setToast(`Error in adding ${visualzationTitle} visualization to the panel`, 'danger'); + setToast(`Error in adding ${visualzationTitle} visualization to the Dashboard`, 'danger'); console.error(err); }); }; @@ -453,7 +505,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onClick={() => setPanelsMenuPopover(true)} disabled={addVizDisabled} > - Panel actions + Dashboard Actions ); @@ -488,7 +540,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { title: 'Panel actions', items: [ { - name: 'Reload panel', + name: 'Reload Dashboard', 'data-test-subj': 'reloadPanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -496,7 +548,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, }, { - name: 'Rename panel', + name: 'Rename Dashboard', 'data-test-subj': 'renamePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -504,15 +556,15 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, }, { - name: 'Duplicate panel', + name: 'Duplicate Dashboard', 'data-test-subj': 'duplicatePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); - clonePanel(); + clonePanelModal(); }, }, { - name: 'Delete panel', + name: 'Delete Dashboard', 'data-test-subj': 'deletePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -523,7 +575,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, ]; - // Fetch the custom panel on Initial Mount + // Fetch the Observability Dashboard on Initial Mount useEffect(() => { fetchCustomPanel(); }, [panelId]); diff --git a/public/components/custom_panels/custom_panel_view_so.tsx b/public/components/custom_panels/custom_panel_view_so.tsx new file mode 100644 index 0000000000..f65fd396f9 --- /dev/null +++ b/public/components/custom_panels/custom_panel_view_so.tsx @@ -0,0 +1,690 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* // eslint-disable no-console */ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { + EuiBreadcrumb, + EuiButton, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, + EuiSuperDatePicker, + EuiTitle, + OnTimeChangeProps, + ShortDate, +} from '@elastic/eui'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { last } from 'lodash'; +import moment from 'moment'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CoreStart } from '../../../../../src/core/public'; +import { CREATE_PANEL_MESSAGE } from '../../../common/constants/custom_panels'; +import { UI_DATE_FORMAT } from '../../../common/constants/shared'; +import { CustomPanelType } from '../../../common/types/custom_panels'; +import { uiSettingsService } from '../../../common/utils'; +import { coreRefs } from '../../framework/core_refs'; +import { PPLReferenceFlyout } from '../common/helpers'; +import { DeleteModal } from '../common/helpers/delete_modal'; +import { Autocomplete } from '../common/search/autocomplete'; +import { onItemSelect, parseGetSuggestions } from '../common/search/autocomplete_logic'; +import { addVisualizationPanel } from './helpers/add_visualization_helper'; +import { AddVisualizationPopover } from './helpers/add_visualization_popover'; +import { getCustomModal } from './helpers/modal_containers'; +import { + convertDateTime, + isDateValid, + isNameValid, + isPPLFilterValid, + prependRecentlyUsedRange, +} from './helpers/utils'; +import { EmptyPanelView } from './panel_modules/empty_panel'; +import { PanelGridSO } from './panel_modules/panel_grid/panel_grid_so'; +import { VisaulizationFlyoutSO } from './panel_modules/visualization_flyout/visualization_flyout_so'; +import { + clonePanel, + deletePanels, + doesNameExist, + fetchPanel, + renameCustomPanel, + selectPanel, + setPanel, + updatePanel, +} from './redux/panel_slice'; +import { useToast } from '../common/toast'; +import PPLService from '../../services/requests/ppl'; +import DSLService from '../../services/requests/dsl'; + +/* + * "CustomPanelsView" module used to render an Observability Dashboard + * + * Props taken in as params are: + * panelId: Name of the panel opened + * page: Page where component is called + * http: http core service + * coreSavedObjects : savedObjects core service + * pplService: ppl requestor service + * dslService: dsl requestor service + * chrome: chrome core service + * parentBreadcrumb: parent breadcrumb + * renameCustomPanel: Rename function for the panel + * deleteCustomPanel: Delete function for the panel + * cloneCustomPanel: Clone function for the panel + * onEditClick: Edit function for visualization + * startTime: Starting time + * endTime: Ending time + * setStartTime: Function to change start time + * setEndTime: Function to change end time + * childBreadcrumbs: Breadcrumbs to extend + * appId: id of application that panel belongs to + * onAddClick: Function for add button instead of add visualization popover + */ + +interface CustomPanelViewProps { + panelId: string; + page: 'app' | 'operationalPanels'; + coreSavedObjects: CoreStart['savedObjects']; + chrome: CoreStart['chrome']; + parentBreadcrumbs: EuiBreadcrumb[]; + cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; + onEditClick: (savedVisualizationId: string) => any; + childBreadcrumbs?: EuiBreadcrumb[]; + appId?: string; + updateAvailabilityVizId?: any; + onAddClick?: any; + pplService: PPLService; + dslService: DSLService; +} + +export const CustomPanelViewSO = (props: CustomPanelViewProps) => { + const { + panelId, + page, + appId, + pplService, + dslService, + chrome, + parentBreadcrumbs, + childBreadcrumbs, + updateAvailabilityVizId, + cloneCustomPanel, + onEditClick, + onAddClick, + } = props; + + const dispatch = useDispatch(); + const { setToast } = useToast(); + + const panel = useSelector(selectPanel); + const [loading, setLoading] = useState(true); + + const [pplFilterValue, setPPLFilterValue] = useState(''); + const [baseQuery, setBaseQuery] = useState(''); + const [onRefresh, setOnRefresh] = useState(false); + + const [inputDisabled, setInputDisabled] = useState(true); + const [addVizDisabled, setAddVizDisabled] = useState(false); + const [editDisabled, setEditDisabled] = useState(false); + const [dateDisabled, setDateDisabled] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle + const [modalLayout, setModalLayout] = useState(); // Modal Layout + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); // Add Visualization Flyout + const [isFlyoutReplacement, setisFlyoutReplacement] = useState(false); + const [replaceVisualizationId, setReplaceVisualizationId] = useState(''); + const [panelsMenuPopover, setPanelsMenuPopover] = useState(false); + const [editActionType, setEditActionType] = useState(''); + const [isHelpFlyoutVisible, setHelpIsFlyoutVisible] = useState(false); + + const appPanel = page === 'app'; + + const closeHelpFlyout = () => { + setAddVizDisabled(false); + setHelpIsFlyoutVisible(false); + }; + + const showHelpFlyout = () => { + setAddVizDisabled(true); + setHelpIsFlyoutVisible(true); + }; + + // DateTimePicker States/add + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); + + const handleQueryChange = (newQuery: string) => { + setPPLFilterValue(newQuery); + }; + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onDatePickerChange = (timeProps: OnTimeChangeProps) => { + const updatedRanges = prependRecentlyUsedRange( + timeProps.start, + timeProps.end, + recentlyUsedRanges + ); + dispatch(updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } }, '', '')); + + setRecentlyUsedRanges(updatedRanges.slice(0, 9)); + onRefreshFilters(timeProps.start, timeProps.end); + }; + + const onDelete = async () => { + dispatch(deletePanels([panel])); + setTimeout(() => { + window.location.assign(`${last(parentBreadcrumbs)!.href}`); + }, 1000); + closeModal(); + }; + + const deletePanel = () => { + setModalLayout( + + ); + showModal(); + }; + + const onRename = async (newCustomPanelName: string) => { + if (await doesNameExist(newCustomPanelName)) { + setToast(`Observability Dashboard with name "${newCustomPanelName}" already exists`, 'danger'); + } else if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + dispatch(renameCustomPanel(newCustomPanelName, panel.id)); + } + closeModal(); + }; + + const renamePanel = () => { + setModalLayout( + getCustomModal( + onRename, + closeModal, + 'Name', + 'Rename Dashboard', + 'Cancel', + 'Rename', + panel.title, + CREATE_PANEL_MESSAGE + ) + ); + showModal(); + }; + + const onClone = async (newCustomPanelName: string) => { + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Observability Dashboard name', 'danger'); + } else { + dispatch(clonePanel(panel, newCustomPanelName)); + } + closeModal(); + }; + + const clonePanelModal = () => { + setModalLayout( + getCustomModal( + onClone, + closeModal, + 'Name', + 'Duplicate Dashboard', + 'Cancel', + 'Duplicate', + panel.title + ' (copy)', + CREATE_PANEL_MESSAGE + ) + ); + showModal(); + }; + + // toggle between panel edit mode + const editPanel = (editType: string) => { + setIsEditing(!isEditing); + if (editType === 'cancel') dispatch(fetchPanel(panelId)); + setEditActionType(editType); + }; + + const closeFlyout = () => { + setIsFlyoutVisible(false); + setAddVizDisabled(false); + checkDisabledInputs(); + }; + + const showFlyout = (isReplacement?: boolean, replaceVizId?: string) => { + setisFlyoutReplacement(isReplacement); + setReplaceVisualizationId(replaceVizId); + setIsFlyoutVisible(true); + setAddVizDisabled(true); + setInputDisabled(true); + }; + + const checkDisabledInputs = () => { + // When not in edit mode and panel has no visualizations + if (panel.visualizations.length === 0 && !isEditing) { + setEditDisabled(true); + setInputDisabled(true); + setAddVizDisabled(false); + setDateDisabled(false); + } + + // When panel has visualizations + if (panel.visualizations.length > 0) { + setEditDisabled(false); + setInputDisabled(false); + setAddVizDisabled(false); + setDateDisabled(false); + } + + // When in edit mode + if (isEditing) { + setEditDisabled(false); + setInputDisabled(true); + setAddVizDisabled(true); + setDateDisabled(true); + } + }; + + const buildBaseQuery = async () => { + // const indices: string[] = []; + // for (let i = 0; i < visualizations.length; i++) { + // const visualizationId = visualizations[i].savedVisualizationId; + // // TODO: create route to get list of visualizations in one call + // const visData: SavedVisualizationType = await fetchVisualizationById( + // http, + // visualizationId, + // (error: VizContainerError) => setToast(error.errorMessage, 'danger') + // ); + + // if (!_.isEmpty(visData)) { + // const moreIndices = parseForIndices(visData.query); + // for (let j = 0; j < moreIndices.length; j++) { + // if (!indices.includes(moreIndices[j])) { + // indices.push(moreIndices[j]); + // } + // } + // } + // } + // setBaseQuery('source = ' + indices.join(', ')); + return; + }; + + const onRefreshFilters = async (start: ShortDate, end: ShortDate) => { + if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast)) { + return; + } + + if (!isPPLFilterValid(pplFilterValue, setToast)) { + console.error(pplFilterValue); + return; + } + + await coreRefs.savedObjectsClient?.update('observability-panel', panelId, { + ...panel, + timeRange: { + to: end, + from: start, + }, + queryFilter: { + query: pplFilterValue, + language: 'ppl', + }, + }); + + setOnRefresh(!onRefresh); + }; + + const cloneVisualization = (visualzationTitle: string, savedVisualizationId: string) => { + addVisualizationToCurrentPanel({ savedVisualizationId, successMsg: `Visualization ${visualzationTitle} successfully added!`, failureMsg: `Error in adding ${visualzationTitle} visualization to the panel` }); + }; + + const cancelButton = ( + editPanel('cancel')} + > + Cancel + + ); + + const saveButton = ( + editPanel('save')}> + Save + + ); + + const editButton = ( + editPanel('edit')} + disabled={editDisabled} + > + Edit + + ); + + const addButton = ( + + Add + + ); + + // Panel Actions Button + const panelActionsButton = ( + setPanelsMenuPopover(true)} + disabled={addVizDisabled} + > + Dashboard Actions + + ); + + const addVisualizationToCurrentPanel = async ({ + savedVisualizationId, + oldVisualizationId, + successMsg, + failureMsg, + }: { + savedVisualizationId: string; + oldVisualizationId?: string; + successMsg: string; + failureMsg: string; + }) => { + const allVisualizations = panel!.visualizations; + + const visualizationsWithNewPanel = addVisualizationPanel( + savedVisualizationId, + oldVisualizationId, + allVisualizations + ); + + const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; + dispatch(updatePanel(updatedPanel, successMsg, failureMsg)); + }; + + const setPanelVisualizations = (newVis) => { + const newPanel: CustomPanelType = { ...panel, visualizations: newVis }; + dispatch(setPanel(newPanel)); + }; + + let flyout; + if (isFlyoutVisible) { + flyout = ( + + ); + } + + let helpFlyout; + if (isHelpFlyoutVisible) { + helpFlyout = ; + } + + const panelActionsMenu: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: 'Panel actions', + items: [ + { + name: 'Reload Dashboard', + 'data-test-subj': 'reloadPanelContextMenuItem', + onClick: () => { + setPanelsMenuPopover(false); + dispatch(fetchPanel(panelId)); + }, + }, + { + name: 'Rename Dashboard', + 'data-test-subj': 'renamePanelContextMenuItem', + onClick: () => { + setPanelsMenuPopover(false); + renamePanel(); + }, + }, + { + name: 'Duplicate Dashboard', + 'data-test-subj': 'duplicatePanelContextMenuItem', + onClick: () => { + setPanelsMenuPopover(false); + clonePanelModal(); + }, + }, + { + name: 'Delete Dashboard', + 'data-test-subj': 'deletePanelContextMenuItem', + onClick: () => { + setPanelsMenuPopover(false); + deletePanel(); + }, + }, + ], + }, + ]; + // Fetch the Observability Dashboard on Initial Mount + useEffect(() => { + setLoading(true); + dispatch(fetchPanel(panelId)); + }, []); + + // Toggle input type (disabled or not disabled) + // Disabled when there no visualizations in panels or when the panel is in edit mode + useEffect(() => { + if (!loading) { + checkDisabledInputs(); + } + }, [isEditing, loading]); + + // Build base query with all of the indices included in the current visualizations + useEffect(() => { + if (loading) { + if (panel.id === props.panelId) setLoading(false); + else return; + } + + checkDisabledInputs(); + buildBaseQuery(); + setLoading(false); + }, [panel, loading]); + + // Edit the breadcrumb when panel name changes + useEffect(() => { + if (!panel) return; + + let newBreadcrumb; + if (childBreadcrumbs) { + newBreadcrumb = childBreadcrumbs; + } else { + newBreadcrumb = [ + { + text: panel.title, + href: `${last(parentBreadcrumbs)!.href}${panelId}`, + }, + ]; + } + chrome.setBreadcrumbs([...parentBreadcrumbs, ...newBreadcrumb]); + }, [panelId, panel]); + + return loading ? ( + <> + ) : ( +
+ + + + {appPanel || ( + <> + + +

{panel?.title}

+
+ + + + Created on {moment(panel?.dateCreated || 0).format(UI_DATE_FORMAT)} +
+ + + {isEditing ? ( + <> + {cancelButton} + {saveButton} + + ) : ( + {editButton} + )} + + setPanelsMenuPopover(false)} + > + + + + + + + + + + )} +
+ + + + + onRefreshFilters(panel.timeRange.from, panel.timeRange.to) + } + dslService={dslService} + getSuggestions={parseGetSuggestions} + onItemSelect={onItemSelect} + isDisabled={inputDisabled} + tabId={'panels-filter'} + placeholder={ + "Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" + } + possibleCommands={[{ label: 'where' }]} + append={ + + PPL + + } + /> + + + + + {appPanel && ( + <> + {isEditing ? ( + <> + {cancelButton} + {saveButton} + + ) : ( + {editButton} + )} + {addButton} + + )} + + + {panel.visualizations.length === 0 && ( + + )} + + +
+
+ {isModalVisible && modalLayout} + {flyout} + {helpFlyout} +
+ ); +}; diff --git a/public/components/custom_panels/helpers/__tests__/utils.test.tsx b/public/components/custom_panels/helpers/__tests__/utils.test.tsx index ba4c6b27bd..9ea806c5b5 100644 --- a/public/components/custom_panels/helpers/__tests__/utils.test.tsx +++ b/public/components/custom_panels/helpers/__tests__/utils.test.tsx @@ -3,20 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import moment from 'moment'; -import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; - -import { - isNameValid, - convertDateTime, - mergeLayoutAndVisualizations, - onTimeChange, - isDateValid, - isPPLFilterValid, - displayVisualization, -} from '../utils'; +import React from 'react'; +import { PPL_DATE_FORMAT } from '../../../../../common/constants/shared'; import { sampleLayout, sampleMergedVisualizations, @@ -26,11 +18,16 @@ import { sampleSavedVisualization, sampleSavedVisualizationForHorizontalBar, sampleSavedVisualizationForLine, - sampleSavedVisualizationForPie, - sampleSavedVisualizationForTreeMap, } from '../../../../../test/panels_constants'; -import { PPL_DATE_FORMAT } from '../../../../../common/constants/shared'; -import React from 'react'; +import { + convertDateTime, + displayVisualization, + isDateValid, + isNameValid, + isPPLFilterValid, + mergeLayoutAndVisualizations, + onTimeChange, +} from '../utils'; describe('Utils helper functions', () => { configure({ adapter: new Adapter() }); @@ -56,23 +53,22 @@ describe('Utils helper functions', () => { }); it('validates onTimeChange function', () => { - const setRecentlyUsedRanges = jest.fn((x) => x); - const setStart = jest.fn(); - const setEnd = jest.fn(); const recentlyUsedRanges: DurationRange[] = []; - onTimeChange( + const result = onTimeChange( '2022-01-30T18:44:40.577Z', '2022-02-25T19:18:33.075Z', - recentlyUsedRanges, - setRecentlyUsedRanges, - setStart, - setEnd + recentlyUsedRanges ); - expect(setRecentlyUsedRanges).toHaveBeenCalledWith([ - { start: '2022-01-30T18:44:40.577Z', end: '2022-02-25T19:18:33.075Z' }, - ]); - expect(setStart).toHaveBeenCalledWith('2022-01-30T18:44:40.577Z'); - expect(setEnd).toHaveBeenCalledWith('2022-02-25T19:18:33.075Z'); + expect(result).toEqual({ + start: '2022-01-30T18:44:40.577Z', + end: '2022-02-25T19:18:33.075Z', + updatedRanges: [ + { + start: '2022-01-30T18:44:40.577Z', + end: '2022-02-25T19:18:33.075Z', + }, + ], + }); }); it('validates isDateValid function', () => { diff --git a/public/components/custom_panels/helpers/add_visualization_helper.ts b/public/components/custom_panels/helpers/add_visualization_helper.ts new file mode 100644 index 0000000000..1c827219b5 --- /dev/null +++ b/public/components/custom_panels/helpers/add_visualization_helper.ts @@ -0,0 +1,125 @@ +import { v4 as uuidv4 } from 'uuid'; +import { BoxType, VisualizationType } from '../../../../common/types/custom_panels'; + +const calculatOverlapArea = (bb1: BoxType, bb2: BoxType) => { + const xLeft = Math.max(bb1.x1, bb2.x1); + const yTop = Math.max(bb1.y1, bb2.y1); + const xRight = Math.min(bb1.x2, bb2.x2); + const yBottom = Math.min(bb1.y2, bb2.y2); + + if (xRight < xLeft || yBottom < yTop) return 0; + return (xRight - xLeft) * (yBottom - yTop); +}; + +const getTotalOverlapArea = (panelVisualizations: VisualizationType[]) => { + const newVizBox = { x1: 0, y1: 0, x2: 6, y2: 4 }; + const currentVizBoxes = panelVisualizations.map((visualization) => { + return { + x1: visualization.x, + y1: visualization.y, + x2: visualization.x + visualization.w, + y2: visualization.y + visualization.h, + }; + }); + + let isOverlapping = 0; + currentVizBoxes.map((viz) => { + isOverlapping += calculatOverlapArea(viz, newVizBox); + }); + return isOverlapping; +}; + +// We want to check if the new visualization being added, can be placed at { x: 0, y: 0, w: 6, h: 4 }; +// To check this we try to calculate overlap between all the current visualizations and new visualization +// if there is no overalap (i.e Total Overlap Area is 0), we place the new viz. in default position +// else, we add it to the bottom of the panel +const getNewVizDimensions = (panelVisualizations: VisualizationType[]) => { + let maxY: number = 0; + let maxYH: number = 0; + + // check if we can place the new visualization at default location + if (getTotalOverlapArea(panelVisualizations) === 0) { + return { x: 0, y: 0, w: 6, h: 4 }; + } + + // else place the new visualization at the bottom of the panel + panelVisualizations.map((panelVisualization: VisualizationType) => { + if (panelVisualization.y >= maxY) { + maxY = panelVisualization.y; + maxYH = panelVisualization.h; + } + }); + + return { x: 0, y: maxY + maxYH, w: 6, h: 4 }; +}; + +// Add Visualization in the Panel +export const addVisualizationPanel = ( + // client: ILegacyScopedClusterClient, + // panelId: string, + savedVisualizationId: string, + oldVisualizationId: string | undefined, + allPanelVisualizations: VisualizationType[] +) => { + try { + // const allPanelVisualizations = await this.getVisualizations(client, panelId); + + let newDimensions; + let visualizationsList = [] as VisualizationType[]; + if (oldVisualizationId === undefined) { + newDimensions = getNewVizDimensions(allPanelVisualizations); + visualizationsList = allPanelVisualizations; + } else { + allPanelVisualizations.map((visualization: VisualizationType) => { + if (visualization.id !== oldVisualizationId) { + visualizationsList.push(visualization); + } else { + newDimensions = { + x: visualization.x, + y: visualization.y, + w: visualization.w, + h: visualization.h, + }; + } + }); + } + const newPanelVisualizations = [ + ...visualizationsList, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId, + ...newDimensions, + }, + ]; + return newPanelVisualizations; + } catch (error) { + throw new Error('Add/Replace Visualization Error:' + error); + } +}; + +// Add Multiple visualizations in a Panel +export const addMultipleVisualizations = ( + savedVisualizationIds: string[], + allPanelVisualizations: VisualizationType[] +) => { + try { + let newDimensions; + let visualizationsList = [...allPanelVisualizations]; + + savedVisualizationIds.map((savedVisualizationId) => { + newDimensions = getNewVizDimensions(visualizationsList); + visualizationsList = [ + ...visualizationsList, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId, + ...newDimensions, + }, + ]; + }); + + return visualizationsList; + } catch (error) { + throw new Error('Add Multiple Visualization Error:' + error); + } +}; diff --git a/public/components/custom_panels/helpers/add_visualization_popover.tsx b/public/components/custom_panels/helpers/add_visualization_popover.tsx index 4dac54ab30..d36fcd149b 100644 --- a/public/components/custom_panels/helpers/add_visualization_popover.tsx +++ b/public/components/custom_panels/helpers/add_visualization_popover.tsx @@ -5,6 +5,12 @@ import { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; import React, { useState } from 'react'; +import { + CREATE_TAB_PARAM, + CREATE_TAB_PARAM_KEY, + TAB_CHART_ID, +} from '../../../../common/constants/explorer'; +import { observabilityLogsID } from '../../../../common/constants/shared'; interface AddVisualizationPopoverProps { showFlyout: (isReplacement?: boolean, replaceVizId?: string) => void; @@ -27,7 +33,9 @@ export const AddVisualizationPopover = ({ const advancedVisualization = () => { closeVizPopover(); - window.location.assign('#/event_analytics/explorer'); + window.location.assign( + `${observabilityLogsID}#/explorer?${CREATE_TAB_PARAM_KEY}=${CREATE_TAB_PARAM[TAB_CHART_ID]}` + ); }; const getVizContextPanels = () => { diff --git a/public/components/custom_panels/helpers/modal_containers.tsx b/public/components/custom_panels/helpers/modal_containers.tsx index 0fb80a5979..cdc93226c9 100644 --- a/public/components/custom_panels/helpers/modal_containers.tsx +++ b/public/components/custom_panels/helpers/modal_containers.tsx @@ -67,14 +67,14 @@ export const getCloneModal = ( return ( -

Do you want to clone this operational panel?

+

Do you want to clone this Observability Dashboard?

); diff --git a/public/components/custom_panels/helpers/panel_state_reducer.ts b/public/components/custom_panels/helpers/panel_state_reducer.ts new file mode 100644 index 0000000000..1fd5bf7aeb --- /dev/null +++ b/public/components/custom_panels/helpers/panel_state_reducer.ts @@ -0,0 +1,15 @@ +import { CUSTOM_PANELS_SAVED_OBJECT_TYPE } from "common/constants/custom_panels" +import { coreRefs } from "public/framework/core_refs" + + +const FETCH = 'fetch' + +/* +** ACTIONS +*/ +const fetchPanel = (id) => ({ type: FETCH, id }) + +export const Actions = { fetchPanel } + + + diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index 4f0c44b4c7..d2ca2a1be5 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -32,7 +32,7 @@ import { getDefaultVisConfig } from '../../event_analytics/utils'; import { Visualization } from '../../visualizations/visualization'; /* - * "Utils" This file contains different reused functions in operational panels + * "Utils" This file contains different reused functions in Observability Dashboards * * isNameValid - Validates string to length > 0 and < 50 * convertDateTime - Converts input datetime string to required format @@ -370,23 +370,18 @@ export const renderCatalogVisualization = async ( }; // Function to store recently used time filters and set start and end time. -export const onTimeChange = ( +export const prependRecentlyUsedRange = ( start: ShortDate, end: ShortDate, - recentlyUsedRanges: DurationRange[], - setRecentlyUsedRanges: React.Dispatch>, - setStart: React.Dispatch>, - setEnd: React.Dispatch> + recentlyUsedRanges: DurationRange[] ) => { - const recentlyUsedRangeObject = recentlyUsedRanges.filter((recentlyUsedRange) => { - const isDuplicate = recentlyUsedRange.start === start && recentlyUsedRange.end === end; - return !isDuplicate; - }); + const deduplicatedRanges = rejectRecentRange(recentlyUsedRanges, { start, end }); - recentlyUsedRangeObject.unshift({ start, end }); - setStart(start); - setEnd(end); - setRecentlyUsedRanges(recentlyUsedRangeObject.slice(0, 9)); + return [{ start, end }, ...deduplicatedRanges]; +}; + +const rejectRecentRange = (rangeList, toReject) => { + return rangeList.filter((r) => !(r.start === toReject.start && r.end === toReject.end)); }; /** @@ -521,3 +516,16 @@ export const displayVisualization = (metaData: any, data: any, type: string) => /> ); }; + +export const onTimeChange = ( + start: ShortDate, + end: ShortDate, + recentlyUsedRanges: DurationRange[] +) => { + const updatedRanges = recentlyUsedRanges.filter((recentlyUsedRange) => { + const isDuplicate = recentlyUsedRange.start === start && recentlyUsedRange.end === end; + return !isDuplicate; + }); + updatedRanges.unshift({ start, end }); + return { start, end, updatedRanges }; +}; diff --git a/public/components/custom_panels/home.tsx b/public/components/custom_panels/home.tsx index eb5db6ef8c..b43ec1cc59 100644 --- a/public/components/custom_panels/home.tsx +++ b/public/components/custom_panels/home.tsx @@ -2,32 +2,46 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ -import { EuiBreadcrumb, EuiGlobalToastList, EuiLink, ShortDate } from '@elastic/eui'; -import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; -import _ from 'lodash'; -import React, { ReactChild, useState } from 'react'; +import { EuiBreadcrumb, ShortDate, htmlIdGenerator } from '@elastic/eui'; +import React, { useState } from 'react'; +import { useDispatch, batch } from 'react-redux'; // eslint-disable-next-line @osd/eslint/module_migration -import { StaticContext, Switch } from 'react-router'; -import { Route, RouteComponentProps, useHistory } from 'react-router-dom'; -import PPLService from '../../services/requests/ppl'; -import DSLService from '../../services/requests/dsl'; -import { CoreStart } from '../../../../../src/core/public'; -import { - CUSTOM_PANELS_API_PREFIX, - CUSTOM_PANELS_DOCUMENTATION_URL, -} from '../../../common/constants/custom_panels'; +import { StaticContext } from 'react-router'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { CoreStart, SavedObjectsStart } from '../../../../../src/core/public'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../common/constants/custom_panels'; import { EVENT_ANALYTICS, + observabilityLogsID, + observabilityPanelsID, OBSERVABILITY_BASE, SAVED_OBJECTS, } from '../../../common/constants/shared'; -import { CustomPanelListType } from '../../../common/types/custom_panels'; -import { ObservabilitySideBar } from '../common/side_nav'; +import DSLService from '../../services/requests/dsl'; +import PPLService from '../../services/requests/ppl'; import { CustomPanelTable } from './custom_panel_table'; import { CustomPanelView } from './custom_panel_view'; -import { isNameValid } from './helpers/utils'; +import { CustomPanelViewSO } from './custom_panel_view_so'; +import { REDIRECT_TAB, TAB_CREATED_TYPE, TAB_ID_TXT_PFX } from '../../../common/constants/explorer'; +import { init as initFields } from '../event_analytics/redux/slices/field_slice'; +import { init as initPatterns } from '../event_analytics/redux/slices/patterns_slice'; +import { init as initQueryResult } from '../event_analytics/redux/slices/query_result_slice'; +import { changeQuery, init as initQuery } from '../event_analytics/redux/slices/query_slice'; +import { addTab, setSelectedQueryTab } from '../event_analytics/redux/slices/query_tab_slice'; +import { + createPanel, + createPanelSample, + createPanelWithVizs, + deletePanel, + fetchPanels, + newPanelTemplate, + uuidRx, +} from './redux/panel_slice'; +import { useToast } from '../common/toast'; +import { coreRefs } from '../../framework/core_refs'; + +// import { ObjectFetcher } from '../common/objectFetcher'; /* * "Home" module is initial page for Operantional Panels @@ -37,16 +51,19 @@ import { isNameValid } from './helpers/utils'; * chrome: chrome core service; * parentBreadcrumb: parent breadcrumb name and link * pplService: ppl requestor service - * renderProps: Props from router + * renderProps: Props from router of parent component + * - Used to calculate path when this component embedded into another (WHY?!) */ interface PanelHomeProps { http: CoreStart['http']; chrome: CoreStart['chrome']; parentBreadcrumbs: EuiBreadcrumb[]; + setBreadcrumbs: (newBreadcrumbs: EuiBreadcrumb[]) => void; pplService: PPLService; dslService: DSLService; renderProps: RouteComponentProps; + coreSavedObjects: SavedObjectsStart; } export const Home = ({ @@ -56,187 +73,56 @@ export const Home = ({ pplService, dslService, renderProps, + coreSavedObjects, + setBreadcrumbs, }: PanelHomeProps) => { - const [customPanelData, setcustomPanelData] = useState([]); - const [toasts, setToasts] = useState([]); const [loading, setLoading] = useState(false); - const [toastRightSide, setToastRightSide] = useState(true); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); - const history = useHistory(); - - const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { - if (!text) text = ''; - setToastRightSide(!side ? true : false); - setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); - }; - const onEditClick = (savedVisualizationId: string) => { - window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); - }; + const dispatch = useDispatch(); - // Fetches all saved Custom Panels - const fetchCustomPanels = () => { - setLoading(true); - http - .get(`${CUSTOM_PANELS_API_PREFIX}/panels`) - .then((res) => { - setcustomPanelData(res.panels); - }) - .catch((err) => { - console.error('Issue in fetching the operational panels', err.body.message); - }); - setLoading(false); - }; + const { setToast } = useToast(); - // Creates a new CustomPanel - const createCustomPanel = (newCustomPanelName: string) => { - if (!isNameValid(newCustomPanelName)) { - setToast('Invalid Operational Panel name', 'danger'); - window.location.assign(`${_.last(parentBreadcrumbs)!.href}`); - return; - } + const customPanelBreadCrumbs = [ + ...parentBreadcrumbs, + { + text: 'Dashboards', + href: `${observabilityPanelsID}#/`, + }, + ]; - return http - .post(`${CUSTOM_PANELS_API_PREFIX}/panels`, { - body: JSON.stringify({ - panelName: newCustomPanelName, - }), - }) - .then(async (res) => { - setToast(`Operational Panel "${newCustomPanelName}" successfully created!`); - window.location.assign(`${_.last(parentBreadcrumbs)!.href}${res.newPanelId}`); - }) - .catch((err) => { - setToast( - 'Please ask your administrator to enable Operational Panels for you.', - 'danger', - - Documentation - - ); - console.error(err); - }); - }; + const addNewTab = async () => { + // get a new tabId + const tabId = htmlIdGenerator(TAB_ID_TXT_PFX)(); - // Renames an existing CustomPanel - const renameCustomPanel = (editedCustomPanelName: string, editedCustomPanelId: string) => { - if (!isNameValid(editedCustomPanelName)) { - setToast('Invalid Custom Panel name', 'danger'); - return Promise.reject(); - } - const renamePanelObject = { - panelId: editedCustomPanelId, - panelName: editedCustomPanelName, - }; + // create a new tab + await batch(() => { + dispatch(initQuery({ tabId })); + dispatch(initQueryResult({ tabId })); + dispatch(initFields({ tabId })); + dispatch(addTab({ tabId })); + dispatch(initPatterns({ tabId })); + }); - return http - .post(`${CUSTOM_PANELS_API_PREFIX}/panels/rename`, { - body: JSON.stringify(renamePanelObject), - }) - .then((res) => { - setcustomPanelData((prevCustomPanelData) => { - const newCustomPanelData = [...prevCustomPanelData]; - const renamedCustomPanel = newCustomPanelData.find( - (customPanel) => customPanel.id === editedCustomPanelId - ); - if (renamedCustomPanel) renamedCustomPanel.name = editedCustomPanelName; - return newCustomPanelData; - }); - setToast(`Operational Panel successfully renamed into "${editedCustomPanelName}"`); - }) - .catch((err) => { - setToast( - 'Error renaming Operational Panel, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); - }); + return tabId; }; - // Clones an existing Custom Panel, return new Custom Panel id - const cloneCustomPanel = ( - clonedCustomPanelName: string, - clonedCustomPanelId: string - ): Promise => { - if (!isNameValid(clonedCustomPanelName)) { - setToast('Invalid Operational Panel name', 'danger'); - return Promise.reject(); - } - const clonePanelObject = { - panelId: clonedCustomPanelId, - panelName: clonedCustomPanelName, - }; - - return http - .post(`${CUSTOM_PANELS_API_PREFIX}/panels/clone`, { - body: JSON.stringify(clonePanelObject), - }) - .then((res) => { - setcustomPanelData((prevCustomPanelData) => { - return [ - ...prevCustomPanelData, - { - name: clonedCustomPanelName, - id: res.clonePanelId, - dateCreated: res.dateCreated, - dateModified: res.dateModified, - }, - ]; - }); - setToast(`Operational Panel "${clonedCustomPanelName}" successfully created!`); - return res.clonePanelId; - }) - .catch((err) => { - setToast( - 'Error cloning Operational Panel, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); - }); - }; - - // Deletes multiple existing Operational Panels - const deleteCustomPanelList = (customPanelIdList: string[], toastMessage: string) => { - const concatList = customPanelIdList.toString(); - return http - .delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList) - .then((res) => { - setcustomPanelData((prevCustomPanelData) => { - return prevCustomPanelData.filter( - (customPanel) => !customPanelIdList.includes(customPanel.id) - ); - }); - setToast(toastMessage); - return res; - }) - .catch((err) => { - setToast( - 'Error deleting Operational Panels, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); - }); - }; - - // Deletes an existing Operational Panel - const deleteCustomPanel = (customPanelId: string, customPanelName: string) => { - return http - .delete(`${CUSTOM_PANELS_API_PREFIX}/panels/` + customPanelId) - .then((res) => { - setcustomPanelData((prevCustomPanelData) => { - return prevCustomPanelData.filter((customPanel) => customPanel.id !== customPanelId); - }); - setToast(`Operational Panel "${customPanelName}" successfully deleted!`); - return res; - }) - .catch((err) => { - setToast( - 'Error deleting Operational Panel, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); - }); + const onEditClick = async (savedVisualizationId: string) => { + // open a new tab in explorer for loading this perticular visualization data to edit + const newTabId = await addNewTab(); + batch(() => { + dispatch( + changeQuery({ + tabId: newTabId, + query: { + [TAB_CREATED_TYPE]: REDIRECT_TAB, + }, + }) + ); + dispatch(setSelectedQueryTab({ tabId: newTabId })); + }); + window.location.assign(`${observabilityLogsID}#/explorer/${savedVisualizationId}`); }; const addSamplePanels = async () => { @@ -271,83 +157,72 @@ export const Home = ({ .get(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/addSampleSavedObjects/panels`) .then((resp) => (savedVisualizationIds = [...resp.savedVizIds])); - await http - .post(`${CUSTOM_PANELS_API_PREFIX}/panels/addSamplePanels`, { - body: JSON.stringify({ - savedVisualizationIds, - }), - }) - .then((res) => { - setcustomPanelData([...customPanelData, ...res.demoPanelsData]); - }); + dispatch(createPanelSample(savedVisualizationIds)); setToast(`Sample panels successfully added.`); } catch (err: any) { setToast('Error adding sample panels.', 'danger'); - console.error(err.body.message); + console.error(err.body?.message || err); } finally { setLoading(false); } }; + const parentPath = renderProps ? renderProps.match.path : ''; + return ( -
- { - setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); - }} - side={toastRightSide ? 'right' : 'left'} - toastLifeTimeMs={6000} - /> + - { - return ( - + { + return ( - - ); - }} - /> - { - return ( - - ); - }} - /> + ); + }} + /> + { + const isSavedObject = !!props.match.params.id.match(uuidRx); + + return isSavedObject ? ( + + ) : ( + + ); + }} + /> -
+ ); }; diff --git a/public/components/custom_panels/panel_modules/empty_panel.tsx b/public/components/custom_panels/panel_modules/empty_panel.tsx index 32a1226b76..dcebe55732 100644 --- a/public/components/custom_panels/panel_modules/empty_panel.tsx +++ b/public/components/custom_panels/panel_modules/empty_panel.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { AddVisualizationPopover } from '../helpers/add_visualization_popover'; /* - * EmptyPanelView - This Sub-component is shown to the user when a operational panel is empty + * EmptyPanelView - This Sub-component is shown to the user when a Observability Dashboard is empty * * Props taken in as params are: * addVizDisabled -> Boolean to enable/disable the add visualization button diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx b/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx new file mode 100644 index 0000000000..a2fa5e02b5 --- /dev/null +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx @@ -0,0 +1,219 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* eslint-disable react-hooks/exhaustive-deps */ + +import _, { forEach } from 'lodash'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout'; +import useObservable from 'react-use/lib/useObservable'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { VisualizationContainer } from '../visualization_container'; +import { VisualizationType } from '../../../../../common/types/custom_panels'; +import './panel_grid.scss'; +import { mergeLayoutAndVisualizations } from '../../helpers/utils'; +import { coreRefs } from '../../../../framework/core_refs'; +import { selectPanel } from '../../redux/panel_slice'; + +// HOC container to provide dynamic width for Grid layout +const ResponsiveGridLayout = WidthProvider(Responsive); + +/* + * PanelGrid - This module is places all visualizations in react-grid-layout + * + * Props taken in as params are: + * chrome: chrome core service; + * panelId: OpenPanel Id + * updateAvailabilityVizId: function to update application if availabilityViz is removed from panel + * panelVisualizations: list of panel visualizations + * setPanelVisualizations: function to set panel visualizations + * editMode: boolean to check if the panel is in edit mode + * startTime: start time in date filter + * endTime: end time in date filter + * onRefresh: boolean value to trigger refresh of visualizations + * cloneVisualization: function to clone a visualization in panel + * pplFilterValue: string with panel PPL filter value + * showFlyout: function to show the flyout + * editActionType: Type of action done while clicking the edit button + */ + +interface PanelGridProps { + chrome: CoreStart['chrome']; + panelId: string; + updateAvailabilityVizId?: any; + panelVisualizations: VisualizationType[]; + setPanelVisualizations: React.Dispatch>; + editMode: boolean; + startTime: string; + endTime: string; + onEditClick: (savedVisualizationId: string) => any; + onRefresh: boolean; + cloneVisualization: (visualzationTitle: string, savedVisualizationId: string) => void; + pplFilterValue: string; + showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; + editActionType: string; + setEditVizId?: any; +} + +export const PanelGridSO = (props: PanelGridProps) => { + const { + chrome, + panelId, + updateAvailabilityVizId, + panelVisualizations, + setPanelVisualizations, + editMode, + startTime, + endTime, + onEditClick, + onRefresh, + cloneVisualization, + pplFilterValue, + showFlyout, + editActionType, + } = props; + + const panel = useSelector(selectPanel); + const [currentLayout, setCurrentLayout] = useState([]); + const [postEditLayout, setPostEditLayout] = useState([]); + const [gridData, setGridData] = useState(panelVisualizations.map(() => <>)); + const isLocked = useObservable(chrome.getIsNavDrawerLocked$()); + + // Reset Size of Visualizations when layout is changed + const layoutChanged = (currLayouts: Layout[], allLayouts: Layouts) => { + window.dispatchEvent(new Event('resize')); + setPostEditLayout(currLayouts); + }; + + const loadVizComponents = () => { + const gridDataComps = panelVisualizations.map( + (panelVisualization: VisualizationType, index) => ( + + ) + ); + setGridData(gridDataComps); + }; + + // Reload the Layout + const reloadLayout = () => { + const tempLayout: Layout[] = panelVisualizations.map((panelVisualization) => { + return { + i: panelVisualization.id, + x: panelVisualization.x, + y: panelVisualization.y, + w: panelVisualization.w, + h: panelVisualization.h, + static: !editMode, + } as Layout; + }); + setCurrentLayout(tempLayout); + }; + + // remove visualization from panel in edit mode + const removeVisualization = (visualizationId: string) => { + const newVisualizationList = _.reject(panelVisualizations, { + id: visualizationId, + }); + mergeLayoutAndVisualizations(postEditLayout, newVisualizationList, setPanelVisualizations); + }; + + const updateLayout = (visualizations, newLayouts) => { + const newVisualizations = []; + forEach(visualizations, (viz) => { + let newviz = { ...viz }; + forEach(newLayouts, (nwlyt) => { + if (viz.id === nwlyt.i) { + newviz = { + ...newviz, + ...nwlyt, + }; + return; + } + }); + newVisualizations.push({ ...newviz }); + }); + return newVisualizations; + }; + + // Save Visualization Layouts when not in edit mode anymore (after users saves the panel) + const saveVisualizationLayouts = async (panelID: string, visualizationParams: any) => { + const newVisualizations = updateLayout(panel.visualizations, visualizationParams); + const updateRes = await coreRefs.savedObjectsClient?.update('observability-panel', panelID, { + ...panel, + visualizations: newVisualizations, + }); + setPanelVisualizations(updateRes?.attributes?.visualizations || []); + }; + + // Update layout whenever user edit gets completed + useEffect(() => { + if (editMode) { + reloadLayout(); + loadVizComponents(); + } + }, [editMode]); + + useEffect(() => { + if (editActionType === 'save') { + const visualizationParams = postEditLayout.map((layout) => + _.omit(layout, ['static', 'moved']) + ); + saveVisualizationLayouts(panelId, visualizationParams); + if (updateAvailabilityVizId) { + updateAvailabilityVizId(panelVisualizations); + } + } + }, [editActionType]); + + // Update layout whenever visualizations are updated + useEffect(() => { + reloadLayout(); + loadVizComponents(); + }, [panelVisualizations]); + + // Reset Size of Panel Grid when Nav Dock is Locked + useEffect(() => { + setTimeout(function () { + window.dispatchEvent(new Event('resize')); + }, 300); + }, [isLocked]); + + useEffect(() => { + loadVizComponents(); + }, [onRefresh]); + + useEffect(() => { + loadVizComponents(); + }, []); + + return ( + + {panelVisualizations.map((panelVisualization: VisualizationType, index) => ( +
{gridData[index]}
+ ))} +
+ ); +}; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx index 971c4821da..7a629952ed 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx @@ -38,7 +38,7 @@ import { CoreStart } from '../../../../../../../src/core/public'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; import { SAVED_VISUALIZATION } from '../../../../../common/constants/explorer'; import { - pplResponse, + PplResponse, SavedVisualizationType, VisualizationType, VizContainerError, @@ -61,7 +61,7 @@ import './visualization_flyout.scss'; * VisaulizationFlyout - This module create a flyout to add visualization * * Props taken in as params are: - * panelId: panel Id of current operational panel + * panelId: panel Id of current Observability Dashboard * closeFlyout: function to close the flyout * start: start time in date filter * end: end time in date filter @@ -112,7 +112,7 @@ export const VisaulizationFlyout = ({ const [newVisualizationTimeField, setNewVisualizationTimeField] = useState(''); const [previewMetaData, setPreviewMetaData] = useState(); const [pplQuery, setPPLQuery] = useState(''); - const [previewData, setPreviewData] = useState({} as pplResponse); + const [previewData, setPreviewData] = useState({} as PplResponse); const [previewArea, setPreviewArea] = useState(<>); const [previewLoading, setPreviewLoading] = useState(false); const [isPreviewError, setIsPreviewError] = useState({} as VizContainerError); @@ -186,7 +186,10 @@ export const VisaulizationFlyout = ({ setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); }) .catch((err) => { - setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + setToast( + `Error in adding ${newVisualizationTitle} visualization to the Dashboard`, + 'danger' + ); console.error(err); }); } else { @@ -367,7 +370,7 @@ export const VisaulizationFlyout = ({ } }) .catch((err) => { - console.error('Issue in fetching the operational panels', err); + console.error('Issue in fetching the Observability Dashboards', err); }); }; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx new file mode 100644 index 0000000000..35e057a831 --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx @@ -0,0 +1,424 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiDatePicker, + EuiDatePickerRange, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFormRow, + EuiIcon, + EuiLoadingChart, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSelect, + EuiSelectOption, + EuiSpacer, + EuiText, + EuiTitle, + EuiToolTip, + ShortDate, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { SAVED_VISUALIZATION } from '../../../../../common/constants/explorer'; +import { + PPLResponse, + SavedVisualizationType, + VisualizationType, + VizContainerError, +} from '../../../../../common/types/custom_panels'; +import { uiSettingsService } from '../../../../../common/utils'; + +import PPLService from '../../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedVisualization } from '../../../../services/saved_objects/saved_object_client/types'; +import { FlyoutContainers } from '../../../common/flyout_containers'; +import { + convertDateTime, + displayVisualization, + getQueryResponse, + isDateValid, + parseSavedVisualizations, +} from '../../helpers/utils'; +import { replaceVizInPanel, selectPanel } from '../../redux/panel_slice'; +import './visualization_flyout.scss'; +import { useToast } from '../../../common/toast'; + +/* + * VisaulizationFlyoutSO - This module create a flyout to add visualization for SavedObjects custom Panels + * + * Props taken in as params are: + * panelId: panel Id of current Observability Dashboard + * closeFlyout: function to close the flyout + * start: start time in date filter + * end: end time in date filter + * savedObjects: savedObjects core service + * pplService: ppl requestor service + * setPanelVisualizations: function set the visualization list in panel + * isFlyoutReplacement: boolean to see if the flyout is trigger for add or replace visualization + * replaceVisualizationId: string id of the visualization to be replaced + */ + +interface VisualizationFlyoutSOProps { + panelId: string; + pplFilterValue: string; + closeFlyout: () => void; + start: ShortDate; + end: ShortDate; + http: CoreStart['http']; + savedObjects: CoreStart['savedObjects']; + pplService: PPLService; + setPanelVisualizations: React.Dispatch>; + isFlyoutReplacement?: boolean | undefined; + replaceVisualizationId?: string | undefined; + appId?: string; + addVisualizationPanel: any; +} + +export const VisaulizationFlyoutSO = ({ + appId = '', + pplFilterValue, + closeFlyout, + start, + end, + pplService, + isFlyoutReplacement, + replaceVisualizationId, + addVisualizationPanel, +}: VisualizationFlyoutSOProps) => { + const dispatch = useDispatch(); + const { setToast } = useToast(); + + const panel = useSelector(selectPanel); + + const [newVisualizationTitle, setNewVisualizationTitle] = useState(''); + const [newVisualizationType, setNewVisualizationType] = useState(''); + const [newVisualizationTimeField, setNewVisualizationTimeField] = useState(''); + const [previewMetaData, setPreviewMetaData] = useState(); + const [pplQuery, setPPLQuery] = useState(''); + const [previewData, setPreviewData] = useState({} as PPLResponse); + const [previewArea, setPreviewArea] = useState(<>); + const [previewLoading, setPreviewLoading] = useState(false); + const [isPreviewError, setIsPreviewError] = useState({} as VizContainerError); + const [savedVisualizations, setSavedVisualizations] = useState([]); + const [visualizationOptions, setVisualizationOptions] = useState([]); + const [selectValue, setSelectValue] = useState(''); + + // DateTimePicker States + const startDate = convertDateTime(start, true, false); + const endDate = convertDateTime(end, false, false); + + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalContent, setModalContent] = useState(<>); + + const closeModal = () => setIsModalVisible(false); + const showModal = (modalType: string) => { + setModalContent( + + + +

{isPreviewError.errorMessage}

+
+
+ + + Error Details + + + {isPreviewError.errorDetails} + + + + + + Close + + +
+ ); + + setIsModalVisible(true); + }; + + const isInputValid = () => { + if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast)) { + return false; + } + + if (selectValue === '') { + setToast('Please make a valid selection', 'danger', undefined); + return false; + } + + return true; + }; + + const addVisualization = () => { + if (!isInputValid()) return; + + if (isFlyoutReplacement) { + dispatch(replaceVizInPanel(panel, replaceVisualizationId, selectValue, newVisualizationTitle)); + } else { + const visualizationsWithNewPanel = addVisualizationPanel({ + savedVisualizationId: selectValue, + onSuccess: `Visualization ${newVisualizationTitle} successfully added!`, + onFailure: `Error in adding ${newVisualizationTitle} visualization to the panel` + }); + } + closeFlyout(); + }; + + const onRefreshPreview = () => { + if (!isInputValid()) return; + + getQueryResponse( + pplService, + pplQuery, + newVisualizationType, + start, + end, + setPreviewData, + setPreviewLoading, + setIsPreviewError, + pplFilterValue, + newVisualizationTimeField + ); + }; + + const timeRange = ( + + endDate} + // date-picker-preview style reduces height, need to add an empty line + // above error message so it does not overlap with DatePicker. + error={['', 'Time range is invalid.']} + > + endDate} + aria-label="Start date" + dateFormat={uiSettingsService.get('dateFormat')} + /> + } + endDateControl={ + endDate} + aria-label="End date" + dateFormat={uiSettingsService.get('dateFormat')} + /> + } + /> + + + ); + + const flyoutHeader = ( + + +

+ {isFlyoutReplacement ? 'Replace visualization' : 'Select existing visualization'} +

+
+
+ ); + + const onChangeSelection = (e: React.ChangeEvent) => { + setSelectValue(e.target.value); + }; + + const emptySavedVisualizations = ( + +

No saved visualizations found!

+
+ ); + + const flyoutBody = + savedVisualizations.length > 0 ? ( + + <> + + + onChangeSelection(e)} + options={visualizationOptions} + value={selectValue} + /> + + + + + +

Preview

+
+
+ + + +
+ + {previewArea} + +
+ ) : ( + + <> +
{'Please use the "create new visualization" option in add visualization menu.'}
+ +
+ ); + + const flyoutFooter = ( + + + + + Cancel + + + + + Add + + + + + ); + + // Fetch all saved visualizations + const fetchSavedVisualizations = async () => { + return SavedObjectsActions.getBulk({ + objectType: [SAVED_VISUALIZATION], + sortOrder: 'desc', + fromIndex: 0, + }) + .then((response) => ({ + visualizations: response.observabilityObjectList.map(parseSavedVisualizations), + })) + .then((res) => { + if (res.visualizations.length > 0) { + setSavedVisualizations(res.visualizations); + const filterAppVis = res.visualizations.filter((vis: SavedVisualizationType) => { + return appId + ? vis.hasOwnProperty('application_id') + ? vis.application_id === appId + : false + : !vis.hasOwnProperty('application_id'); + }); + setVisualizationOptions( + filterAppVis.map((visualization: SavedVisualizationType) => { + return { value: visualization.id, text: visualization.name }; + }) + ); + } + }) + .catch((err) => { + console.error('Issue in fetching the Observability Dashboards', err); + }); + }; + + useEffect(() => { + const previewTemplate = ( + <> + {timeRange} + + + {previewLoading ? ( + + ) : !_.isEmpty(isPreviewError) ? ( +
+ + + +

{isPreviewError.errorMessage}

+
+ {isPreviewError.hasOwnProperty('errorDetails') && + isPreviewError.errorDetails !== '' ? ( + showModal('errorModal')} size="s"> + See error details + + ) : ( + <> + )} +
+ ) : ( +
+ {displayVisualization(previewMetaData, previewData, newVisualizationType)} +
+ )} +
+
+ + ); + setPreviewArea(previewTemplate); + }, [previewLoading]); + + // On change of selected visualization change options + useEffect(() => { + for (let i = 0; i < savedVisualizations.length; i++) { + const visualization = savedVisualizations[i]; + if (visualization.id === selectValue) { + setPPLQuery(visualization.query); + setNewVisualizationTitle(visualization.name); + setNewVisualizationType(visualization.type); + setPreviewMetaData(visualization); + setNewVisualizationTimeField(visualization.timeField); + break; + } + } + }, [selectValue]); + + // load saved visualizations + useEffect(() => { + fetchSavedVisualizations(); + }, []); + + return ( + <> + + {isModalVisible && modalContent} + + ); +}; diff --git a/public/components/custom_panels/redux/panel_slice.ts b/public/components/custom_panels/redux/panel_slice.ts new file mode 100644 index 0000000000..cba05e80ad --- /dev/null +++ b/public/components/custom_panels/redux/panel_slice.ts @@ -0,0 +1,342 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { async, concat, from, Observable, of } from 'rxjs'; +import { map, mergeMap, tap, toArray } from 'rxjs/operators'; +import { forEach, last } from 'lodash'; +import { + CUSTOM_PANELS_API_PREFIX, + CUSTOM_PANELS_SAVED_OBJECT_TYPE, + CUSTOM_PANEL_SLICE, + createDemoPanel, +} from '../../../../common/constants/custom_panels'; +import { + CustomPanelListType, + CustomPanelType, + ObservabilityPanelAttrs, + PanelType, + VisualizationType, +} from '../../../../common/types/custom_panels'; +import { coreRefs } from '../../../framework/core_refs'; +import { SavedObject, SimpleSavedObject } from '../../../../../../src/core/public'; +import { isNameValid } from '../helpers/utils'; +import { samplePanelName } from '../../../../common/constants/custom_panels'; +import { + addMultipleVisualizations, + addVisualizationPanel, +} from '../helpers/add_visualization_helper'; +import { useToast } from '../../../../public/components/common/toast'; +import { htmlIdGenerator } from '@elastic/eui'; + +interface InitialState { + id: string; + panel: CustomPanelType; + panelList: CustomPanelType[]; +} + +export const newPanelTemplate = (newName): PanelType => ({ + title: newName, + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + visualizations: [], + queryFilter: { language: '', query: '' }, + timeRange: { from: 'now-1d', to: 'now' }, +}); + +const initialState: InitialState = { + id: '', + panel: newPanelTemplate(''), + panelList: [], + loadingFlag: false, +}; + +export const panelSlice = createSlice({ + name: 'customPanel', + initialState, + reducers: { + setPanelId: (state, action) => ({ ...state, id: action.payload }), + + setPanel: (state, action) => { + return { ...state, panel: action.payload }; + }, + + setPanelList: (state, action) => { + return { ...state, panelList: action.payload }; + }, + }, +}); + +export const { setPanel, setPanelList } = panelSlice.actions; + +export const panelReducer = panelSlice.reducer; + +export const selectPanel = createSelector( + (rootState) => rootState.customPanel.panel, + (panel) => normalizedPanel(panel) +); + +const normalizedPanel = (panel: CustomPanelType): CustomPanelType => ({ + ...newPanelTemplate(''), + ...panel, +}); + +export const selectPanelList = (rootState): CustomPanelType[] => rootState.customPanel.panelList; + +const { setToast } = useToast(); + +/* + ** ASYNC DISPATCH FUNCTIONS + */ + +const fetchSavedObjectPanels$ = () => + from(savedObjectPanelsClient.find()).pipe( + mergeMap((res) => res.savedObjects), + map(savedObjectToCustomPanel) + ); + +const fetchObservabilityPanels$ = () => + of(coreRefs.http.get(`${CUSTOM_PANELS_API_PREFIX}/panels`)).pipe( + mergeMap((res) => res), + mergeMap((res) => res.panels as ObservabilityPanelAttrs[]), + map((p: ObservabilityPanelAttrs) => ({ ...p, title: p.name, savedObject: false })) + ); + +// Fetches all saved Custom Panels +const fetchCustomPanels = async () => { + const panels$: Observable = concat( + fetchSavedObjectPanels$(), + fetchObservabilityPanels$() + ).pipe( + map((res) => { + return res as CustomPanelListType; + }) + ); + + return panels$.pipe(toArray()).toPromise(); +}; + +export const fetchPanels = () => async (dispatch, getState) => { + const panels = await fetchCustomPanels(); + dispatch(setPanelList(panels)); +}; + +export const fetchPanel = (id) => async (dispatch, getState) => { + const soPanel = await savedObjectPanelsClient.get(id); + const panel = savedObjectToCustomPanel(soPanel); + dispatch(setPanel(panel)); +}; + +export const fetchVisualization = () => (dispatch, getState) => { }; + +const updateLegacyPanel = (panel: CustomPanelType) => + coreRefs.http!.post(`${CUSTOM_PANELS_API_PREFIX}/panels/update`, { + body: JSON.stringify({ panelId: panel.id, panel: panel as PanelType }), + }); + +const updateSavedObjectPanel = (panel: CustomPanelType) => savedObjectPanelsClient.update(panel); + +export const uuidRx = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; + +export const isUuid = (id) => !!id.match(uuidRx); + +export const doesNameExist = async (newCustomPanelName: string) => { + const panels = await fetchCustomPanels(); + return (panels.some(({ title }: {title: string}) => title === newCustomPanelName)); +} +export const updatePanel = (panel: CustomPanelType, successMsg: string, failureMsg: string) => async (dispatch, getState) => { + try { + if (isUuid(panel.id)) await updateSavedObjectPanel(panel); + else await updateLegacyPanel(panel); + if (successMsg) { + setToast(successMsg) + } + dispatch(setPanel(panel)); + const panelList = getState().customPanel.panelList.map((p) => (p.id === panel.id ? panel : p)); + dispatch(setPanelList(panelList)); + } catch (e) { + if (failureMsg) { + setToast(failureMsg, 'danger') + } + console.error(e); + } +}; + +export const addVizToPanels = (panels, vizId) => async (dispatch, getState) => { + forEach(panels, (oldPanel) => { + const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.panel.id); + + const allVisualizations = panel!.visualizations; + + const visualizationsWithNewPanel = addVisualizationPanel(vizId, undefined, allVisualizations); + + const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; + dispatch(updatePanel(updatedPanel, '', '')); + }); +}; + +export const addMultipleVizToPanels = (panels, vizIds) => async (dispatch, getState) => { + forEach(panels, (oldPanel) => { + const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.panel.id); + + const allVisualizations = panel!.visualizations; + + const visualizationsWithNewPanel = addMultipleVisualizations(vizIds, allVisualizations); + + const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; + dispatch(updatePanel(updatedPanel, '', '')); + }); +}; + +export const replaceVizInPanel = (oldPanel, oldVizId, vizId, newVisualizationTitle) => async (dispatch, getState) => { + const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.id); + + const allVisualizations = panel!.visualizations; + + const visualizationsWithNewPanel = addVisualizationPanel(vizId, oldVizId, allVisualizations); + + const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; + + dispatch(updatePanel(updatedPanel, `Visualization ${newVisualizationTitle} successfully added!`, `Error in adding ${newVisualizationTitle} visualization to the panel`)); +}; + +const deletePanelSO = (customPanelIdList: string[]) => { + const soPanelIds = customPanelIdList.filter((id) => isUuid(id)); + return Promise.all(soPanelIds.map((id) => savedObjectPanelsClient.delete(id))); +}; + +const deleteLegacyPanels = (customPanelIdList: string[]) => { + const panelIds = customPanelIdList.filter((id) => !isUuid(id)); + if (panelIds.length === 0) return; + + const concatList = panelIds.toString(); + return coreRefs.http!.delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList); +}; + +export const deletePanels = (panelsToDelete: CustomPanelType[]) => async (dispatch, getState) => { + const toastMessage = `Observability Dashboard${panelsToDelete.length > 1 ? 's' : ' ' + panelsToDelete[0].title + } successfully deleted!`; + try { + const ids = panelsToDelete.map((p) => p.id); + await Promise.all([deleteLegacyPanels(ids), deletePanelSO(ids)]); + + const panelList: CustomPanelType[] = getState().customPanel.panelList.filter( + (p) => !ids.includes(p.id) + ); + dispatch(setPanelList(panelList)); + setToast(toastMessage); + } catch (e) { + setToast( + 'Error deleting Observability Dashboards, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } +}; + +export const createPanel = (panel) => async (dispatch, getState) => { + try { + const newSOPanel = await savedObjectPanelsClient.create(panel); + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); + setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + window.location.replace(`#/${newPanel.id}`); + } catch (e) { + setToast( + 'Error occurred while creating Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } +}; + +export const createPanelSample = (vizIds) => async (dispatch, getState) => { + const samplePanel = { + ...createDemoPanel(vizIds), + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + title: htmlIdGenerator(samplePanelName)(), + }; + const newSOPanel = await savedObjectPanelsClient.create(samplePanel); + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); +}; + +export const clonePanel = (panel, newPanelName) => async (dispatch, getState) => { + try { + const { id, ...panelCopy } = { + ...panel, + title: newPanelName, + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + } as PanelType; + + const newSOPanel = await savedObjectPanelsClient.create(panelCopy); + + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); + dispatch(setPanel(newPanel)); + setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + window.location.replace(`#/${newPanel.id}`); + } catch (e) { + setToast( + 'Error cloning Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } +}; + +const saveRenamedPanel = async (id, name) => { + const renamePanelObject = { + panelId: id, + panelName: name, + }; + + return http.post(`${CUSTOM_PANELS_API_PREFIX}/panels/rename`, { + body: JSON.stringify(renamePanelObject), + }); +}; + +const saveRenamedPanelSO = async (id, name) => { + const panel: SavedObject = await coreRefs.savedObjectsClient!.get( + CUSTOM_PANELS_SAVED_OBJECT_TYPE, + id + ); + panel.title = name; + await coreRefs.savedObjectsClient!.update(CUSTOM_PANELS_SAVED_OBJECT_TYPE, id, panel); +}; + +// Renames an existing CustomPanel +export const renameCustomPanel = (editedCustomPanelName: string, id: string) => async ( + dispatch, + getState +) => { + const panel = getState().customPanel.panelList.find((p) => p.id === id); + const updatedPanel = { ...panel, title: editedCustomPanelName }; + dispatch(updatePanel(updatedPanel, `Observability Dashboard successfully renamed into "${editedCustomPanelName}"`, 'Error renaming Observability Dashboard, please make sure you have the correct permission.')) +}; + +/* + ** UTILITY FUNCTIONS + */ +const savedObjectToCustomPanel = (so: SimpleSavedObject): CustomPanelType => ({ + id: so.id, + ...so.attributes, + savedObject: true, +}); + +const savedObjectPanelsClient = { + find: (options) => + coreRefs.savedObjectsClient!.find({ type: CUSTOM_PANELS_SAVED_OBJECT_TYPE, ...options }), + delete: (id) => coreRefs.savedObjectsClient!.delete(CUSTOM_PANELS_SAVED_OBJECT_TYPE, id), + update: (panel) => + coreRefs.savedObjectsClient!.update(CUSTOM_PANELS_SAVED_OBJECT_TYPE, panel.id, panel), + get: (id) => coreRefs.savedObjectsClient!.get(CUSTOM_PANELS_SAVED_OBJECT_TYPE, id), + create: (panel) => coreRefs.savedObjectsClient!.create(CUSTOM_PANELS_SAVED_OBJECT_TYPE, panel), +}; diff --git a/public/components/event_analytics/explorer/events_views/docViewer.tsx b/public/components/event_analytics/explorer/events_views/docViewer.tsx index 581b30aeb3..85d737718f 100644 --- a/public/components/event_analytics/explorer/events_views/docViewer.tsx +++ b/public/components/event_analytics/explorer/events_views/docViewer.tsx @@ -20,6 +20,7 @@ import { HttpSetup } from '../../../../../../../src/core/public'; import { TraceBlock } from './trace_block/trace_block'; import { OTEL_TRACE_ID } from '../../../../../common/constants/explorer'; import { isValidTraceId } from '../../utils'; +import { observabilityTracesID } from '../../../../../common/constants/shared'; interface IDocViewerProps { http: HttpSetup; @@ -102,7 +103,7 @@ export function DocViewer(props: IDocViewerProps) { setTracesLink( diff --git a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx index beba69089c..e7183cc4a0 100644 --- a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx +++ b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx @@ -123,14 +123,16 @@ export const DocFlyout = ({ const flyoutBody = ( - {populateDataGrid( - explorerFields, - getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds}, - getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds} - )} - +
+ {populateDataGrid( + explorerFields, + getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), + {memorizedTds}, + getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), + {memorizedTds} + )} + +
); diff --git a/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx b/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx index 4c67c3e199..e7fe8d9899 100644 --- a/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx +++ b/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx @@ -124,9 +124,11 @@ export const SurroundingFlyout = ({ }; const loadButton = (typeOfDocs: 'new' | 'old') => { - typeOfDocs === 'new' - ? loadData(typeOfDocs, numNewEvents + 5) - : loadData(typeOfDocs, valueOldEvents + 5); + if (typeOfDocs === 'new') { + loadData(typeOfDocs, numNewEvents + 5); + } else { + loadData(typeOfDocs, valueOldEvents + 5); + } }; const handleKeyDown = ( @@ -139,11 +141,11 @@ export const SurroundingFlyout = ({ }; const onChangeNewEvents = (e: React.ChangeEvent) => { - setNumNewEvents(parseInt(e.target.value)); + setNumNewEvents(parseInt(e.target.value, 10)); }; const onChangeOldEvents = (e: React.ChangeEvent) => { - setNumOldEvents(parseInt(e.target.value)); + setNumOldEvents(parseInt(e.target.value, 10)); }; const flyoutHeader = ( @@ -227,35 +229,37 @@ export const SurroundingFlyout = ({ const flyoutBody = ( - {getInputForm('arrowUp', onChangeNewEvents, numNewEvents, 'new')} - -
- {newEventsError !== '' && ( - - )} -
- {populateDataGrid( - explorerFields, - getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), - <> - {newEventsData} - {memorizedTds} - {oldEventsData} - , - getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), - <> - {newEventsData} - {memorizedTds} - {oldEventsData} - - )} -
- {oldEventsError !== '' && ( - +
+ {getInputForm('arrowUp', onChangeNewEvents, numNewEvents, 'new')} + +
+ {newEventsError !== '' && ( + + )} +
+ {populateDataGrid( + explorerFields, + getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), + <> + {newEventsData} + {memorizedTds} + {oldEventsData} + , + getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), + <> + {newEventsData} + {memorizedTds} + {oldEventsData} + )} +
+ {oldEventsError !== '' && ( + + )} +
+ + {getInputForm('arrowDown', onChangeOldEvents, valueOldEvents, 'old')}
- - {getInputForm('arrowDown', onChangeOldEvents, valueOldEvents, 'old')} ); diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 32423d6ffe..e75652f600 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -284,7 +284,7 @@ export const Explorer = ({ }, { appBaseQuery, - query: rawQueryStr, + query: curQuery, startingTime, endingTime, isLiveTailOn: isLiveTailOnRef.current, diff --git a/public/components/event_analytics/explorer/log_explorer.tsx b/public/components/event_analytics/explorer/log_explorer.tsx index ac9aa69bd9..4fced3eeaf 100644 --- a/public/components/event_analytics/explorer/log_explorer.tsx +++ b/public/components/event_analytics/explorer/log_explorer.tsx @@ -15,6 +15,7 @@ import $ from 'jquery'; import { isEmpty, map } from 'lodash'; import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { LogExplorerRouterContext } from '..'; import { APP_ANALYTICS_TAB_ID_REGEX, @@ -53,11 +54,11 @@ export const LogExplorer = ({ setToast, savedObjectId, getExistingEmptyTab, - history, notifications, http, queryManager, }: ILogExplorerProps) => { + const history = useHistory(); const routerContext = useContext(LogExplorerRouterContext); const dispatch = useDispatch(); const tabIds = useSelector(selectQueryTabs).queryTabIds.filter( @@ -92,9 +93,7 @@ export const LogExplorer = ({ }, [tabIds]); const handleTabClick = (selectedTab: EuiTabbedContentTab) => { - history.replace( - `/event_analytics/explorer/${queryRef.current![selectedTab.id][SAVED_OBJECT_ID] || ''}` - ); + history.replace(`/explorer/${queryRef.current![selectedTab.id][SAVED_OBJECT_ID] || ''}`); dispatch(setSelectedQueryTab({ tabId: selectedTab.id })); }; diff --git a/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx b/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx index 26f34c6323..5a149e0c73 100644 --- a/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx +++ b/public/components/event_analytics/explorer/log_patterns/log_patterns.tsx @@ -3,22 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useContext } from 'react'; -import { useDispatch, connect } from 'react-redux'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useContext, useState } from 'react'; +import { connect, useDispatch } from 'react-redux'; import { FILTERED_PATTERN, PATTERN_REGEX, - RAW_QUERY, PPL_DEFAULT_PATTERN_REGEX_FILETER, + RAW_QUERY, } from '../../../../../common/constants/explorer'; -import { PatternsHeader } from './patterns_header'; -import { PatternsTable } from './patterns_table'; -import { selectPatterns } from '../../redux/slices/patterns_slice'; +import { PatternTableData, Query as IQuery } from '../../../../../common/types/explorer'; import { TabContext, useFetchPatterns } from '../../hooks'; +import { selectPatterns } from '../../redux/slices/patterns_slice'; import { changeQuery, selectQueries } from '../../redux/slices/query_slice'; import { formatError } from '../../utils'; -import { Query as IQuery, PatternTableData } from '../../../../../common/types/explorer'; +import { PatternsHeader } from './patterns_header'; +import { PatternsTable } from './patterns_table'; export interface LogPatternProps { selectedIntervalUnit: { @@ -41,7 +41,7 @@ const EventPatterns = ({ const dispatch = useDispatch(); const { tabId, pplService, notifications } = useContext(TabContext); const patternsData = patterns[tabId]; - const [viewLogPatterns, setViewLogPatterns] = useState(false); + const [viewLogPatterns, setViewLogPatterns] = useState(true); const [isPatternConfigPopoverOpen, setIsPatternConfigPopoverOpen] = useState(false); const [patternRegexInput, setPatternRegexInput] = useState(PPL_DEFAULT_PATTERN_REGEX_FILETER); const { isEventsLoading: isPatternLoading, getPatterns } = useFetchPatterns({ @@ -99,16 +99,14 @@ const EventPatterns = ({ gutterSize="xs" > - {viewLogPatterns && ( - - )} + diff --git a/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap b/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap index acc9e820ea..cbc6c696c2 100644 --- a/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap +++ b/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap @@ -1,553 +1,608 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Saved query table component Renders saved query table 1`] = ` - + - -

- Custom operational dashboards/application -

-
- -
+ Custom operational dashboards/application + + +
- -
- +
- -
- + - - - - + } + color="hollow" + iconOnClick={[Function]} + iconOnClickAriaLabel="Remove [Logs] Web traffic Panel from selection in this group" + iconSide="right" + iconType="cross" + panel={ + Object { + "dateCreated": 1637781403888, + "dateModified": 1637781403888, + "id": "uRZgU30B661cwDZT-ILw", + "name": "[Logs] Web traffic Panel", + } + } + title="[Logs] Web traffic Panel" + > + - [Logs] Web traffic Panel - - + > + + + + + - - - - - - + + + + - - - - + } + color="hollow" + iconOnClick={[Function]} + iconOnClickAriaLabel="Remove [Logs] Web traffic Panel 2 from selection in this group" + iconSide="right" + iconType="cross" + panel={ + Object { + "dateCreated": 1637781403888, + "dateModified": 1637781403888, + "id": "uRZgU30B661cwDZT-ILw", + "name": "[Logs] Web traffic Panel", + } + } + title="[Logs] Web traffic Panel 2" + > + - [Logs] Web traffic Panel 2 - - + > + + + + + - - - - - - -
+ + + + -
-
-
-
- + +
+
+
+
+ -
- - + + + + + - - - - - -
-
+ +
+ +
-
- - -
- - -
+ +
+ + - Search existing dashboards or applications by name -
- +
+ Search existing dashboards or applications by name +
+ +
- - - -

+ - Name -

-
- -
+ Name + + +
- - -
- - - - + + + + +
-
- - - -
+ + - Name for your savings -
-
+
+ Name for your savings +
+ +
- -
- -
+
- - - -
- + + Save as Metric - - - Save as Metric - -
-
-
-
+
+ + + +
- -
- + + + `; diff --git a/public/components/event_analytics/explorer/save_panel/__tests__/save_panel.test.tsx b/public/components/event_analytics/explorer/save_panel/__tests__/save_panel.test.tsx index a35b7b58ac..0ffab1acbc 100644 --- a/public/components/event_analytics/explorer/save_panel/__tests__/save_panel.test.tsx +++ b/public/components/event_analytics/explorer/save_panel/__tests__/save_panel.test.tsx @@ -3,46 +3,60 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { applyMiddleware, createStore } from '@reduxjs/toolkit'; +import { waitFor } from '@testing-library/react'; 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 { Provider } from 'react-redux'; +import thunk from 'redux-thunk'; 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'; +import { coreRefs } from '../../../../../framework/core_refs'; +import { rootReducer } from '../../../../../framework/redux/reducers'; +import SavedObjects from '../../../../../services/saved_objects/event_analytics/saved_objects'; +import { SavePanel } from '../save_panel'; describe('Saved query table component', () => { configure({ adapter: new Adapter() }); + const store = createStore(rootReducer, applyMiddleware(thunk)); + coreRefs.savedObjectsClient.find = jest.fn(() => + Promise.resolve({ + savedObjects: [], + then: () => Promise.resolve(), + }) + ); it('Renders saved query table', async () => { const handleNameChange = jest.fn(); const handleOptionChange = jest.fn(); const setMetricLabel = 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/public/components/event_analytics/explorer/save_panel/save_panel.tsx b/public/components/event_analytics/explorer/save_panel/save_panel.tsx index 3f4d01cd19..d6ee5940c4 100644 --- a/public/components/event_analytics/explorer/save_panel/save_panel.tsx +++ b/public/components/event_analytics/explorer/save_panel/save_panel.tsx @@ -14,7 +14,12 @@ import { } from '@elastic/eui'; import { useEffect } from 'react'; import { isEmpty } from 'lodash'; +import { useDispatch, useSelector } from 'react-redux'; import SavedObjects from '../../../../services/saved_objects/event_analytics/saved_objects'; +import { + fetchPanels, + selectPanelList, +} from '../../../../../public/components/custom_panels/redux/panel_slice'; interface ISavedPanelProps { selectedOptions: any; @@ -45,23 +50,16 @@ export const SavePanel = ({ setSubType, isSaveAsMetricEnabled, }: ISavedPanelProps) => { - const [options, setOptions] = useState([]); const [checked, setChecked] = useState(false); const [svpnlError, setSvpnlError] = useState(null); - const getCustomPabnelList = async (svobj: SavedObjects) => { - const optionRes = await svobj - .fetchCustomPanels() - .then((res: any) => { - return res; - }) - .catch((error: any) => setSvpnlError(error)); - setOptions(optionRes?.panels || []); - }; + const customPanels = useSelector(selectPanelList); + + const dispatch = useDispatch(); useEffect(() => { - getCustomPabnelList(savedObjects); - }); + dispatch(fetchPanels()); + }, []); const onToggleChange = (e: { target: { checked: React.SetStateAction } }) => { setChecked(e.target.checked); @@ -86,10 +84,10 @@ export const SavePanel = ({ handleOptionChange(daOptions); }} selectedOptions={selectedOptions} - options={options.map((option: CustomPanelOptions) => { + options={customPanels.map((option: any) => { return { panel: option, - label: option.name, + label: option.title, }; })} isClearable={true} diff --git a/public/components/event_analytics/home/home.tsx b/public/components/event_analytics/home/home.tsx index f69489dfc5..b53564ddb3 100644 --- a/public/components/event_analytics/home/home.tsx +++ b/public/components/event_analytics/home/home.tsx @@ -49,10 +49,7 @@ import { ExplorerData as IExplorerData, IQuery, } from '../../../../common/types/explorer'; -import { getOSDSavedObjectsClient } from '../../../../common/utils'; import SavedObjects from '../../../services/saved_objects/event_analytics/saved_objects'; -import { OSDSavedVisualizationClient } from '../../../services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization'; -import { PPLSavedQueryClient } from '../../../services/saved_objects/saved_object_client/ppl'; import { SavedObjectsActions } from '../../../services/saved_objects/saved_object_client/saved_objects_actions'; import { ObservabilitySavedObject } from '../../../services/saved_objects/saved_object_client/types'; import { getSampleDataModal } from '../../common/helpers/add_sample_modal'; @@ -197,7 +194,7 @@ const EventAnalyticsHome = (props: IHomeProps) => { await dispatchInitialData(newTabId); // redirect to explorer - history.push('/event_analytics/explorer'); + history.push('/explorer'); }; const handleQueryChange = async (query: string) => setSearchQuery(query); @@ -223,7 +220,7 @@ const EventAnalyticsHome = (props: IHomeProps) => { dispatch(setSelectedQueryTab({ tabId: newTabId })); }); // redirect to explorer - history.push(`/event_analytics/explorer/${objectId}`); + history.push(`/explorer/${objectId}`); }; const addSampledata = async () => { @@ -334,7 +331,7 @@ const EventAnalyticsHome = (props: IHomeProps) => { key="redirect" onClick={() => { setIsActionsPopoverOpen(false); - history.push(`/event_analytics/explorer`); + history.push(`/explorer`); }} data-test-subj="eventHomeAction__explorer" > @@ -359,7 +356,7 @@ const EventAnalyticsHome = (props: IHomeProps) => { -

Event analytics

+

Logs

@@ -456,7 +453,7 @@ const EventAnalyticsHome = (props: IHomeProps) => { history.push(`/event_analytics/explorer`)} + onClick={() => history.push(`/explorer`)} data-test-subj="actionEventExplorer" > Event Explorer diff --git a/public/components/event_analytics/index.tsx b/public/components/event_analytics/index.tsx index 711b891d12..69c469af1e 100644 --- a/public/components/event_analytics/index.tsx +++ b/public/components/event_analytics/index.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import React, { createContext, ReactChild, useState } from 'react'; import { HashRouter, Route, RouteComponentProps, Switch, useHistory } from 'react-router-dom'; import { RAW_QUERY } from '../../../common/constants/explorer'; -import { ObservabilitySideBar } from '../common/side_nav'; +import '../../variables.scss'; import { LogExplorer } from './explorer/log_explorer'; import { Home as EventExplorerHome } from './home/home'; @@ -29,14 +29,14 @@ export const EventAnalytics = ({ http, notifications, queryManager, + setBreadcrumbs, ...props }: EventAnalyticsProps) => { - const history = useHistory(); const [toasts, setToasts] = useState([]); const eventAnalyticsBreadcrumb = { - text: 'Event analytics', - href: '#/event_analytics', + text: 'Logs', + href: '#/', }; const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { @@ -68,14 +68,14 @@ export const EventAnalytics = ({ { - chrome.setBreadcrumbs([ + setBreadcrumbs([ ...parentBreadcrumbs, eventAnalyticsBreadcrumb, { text: 'Explorer', - href: `#/event_analytics/explorer`, + href: `#/explorer`, }, ]); return ( @@ -94,7 +94,6 @@ export const EventAnalytics = ({ http={http} setToast={setToast} getExistingEmptyTab={getExistingEmptyTab} - history={history} notifications={notifications} queryManager={queryManager} /> @@ -104,27 +103,25 @@ export const EventAnalytics = ({ /> { - chrome.setBreadcrumbs([ + setBreadcrumbs([ ...parentBreadcrumbs, eventAnalyticsBreadcrumb, { text: 'Home', - href: '#/event_analytics', + href: '#/', }, ]); return ( - - - + ); }} /> diff --git a/public/components/index.tsx b/public/components/index.tsx index eec21a18b2..8191752974 100644 --- a/public/components/index.tsx +++ b/public/components/index.tsx @@ -18,7 +18,8 @@ export const Observability = ( dslService: any, savedObjects: any, timestampUtils: any, - queryManager: QueryManager + queryManager: QueryManager, + startPage: string ) => { ReactDOM.render( , AppMountParametersProp.element ); diff --git a/public/components/metrics/index.tsx b/public/components/metrics/index.tsx index 0647cdaba3..7d7eef327f 100644 --- a/public/components/metrics/index.tsx +++ b/public/components/metrics/index.tsx @@ -15,9 +15,10 @@ import { } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import React, { useEffect, useState } from 'react'; -import { Route, RouteComponentProps } from 'react-router-dom'; +import { HashRouter, Route, RouteComponentProps } from 'react-router-dom'; import classNames from 'classnames'; import { StaticContext } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { ChromeBreadcrumb, CoreStart, Toast } from '../../../../../src/core/public'; import { onTimeChange } from './helpers/utils'; import { Sidebar } from './sidebar/sidebar'; @@ -26,10 +27,10 @@ import PPLService from '../../services/requests/ppl'; import { TopMenu } from './top_menu/top_menu'; import { MetricType } from '../../../common/types/metrics'; import { MetricsGrid } from './view/metrics_grid'; -import { useSelector } from 'react-redux'; import { metricsLayoutSelector, selectedMetricsSelector } from './redux/slices/metrics_slice'; import { resolutionOptions } from '../../../common/constants/metrics'; import SavedObjects from '../../services/saved_objects/event_analytics/saved_objects'; +import { observabilityLogsID } from '../../../common/constants/shared'; interface MetricsProps { http: CoreStart['http']; @@ -38,6 +39,7 @@ interface MetricsProps { renderProps: RouteComponentProps; pplService: PPLService; savedObjects: SavedObjects; + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; } export const Home = ({ @@ -82,6 +84,7 @@ export const Home = ({ }; const onRefreshFilters = (startTime: ShortDate, endTime: ShortDate) => { + // eslint-disable-line if (spanValue < 1) { setToast('Please add a valid span interval', 'danger'); return; @@ -102,7 +105,7 @@ export const Home = ({ }; const onEditClick = (savedVisualizationId: string) => { - window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); + window.location.assign(`${observabilityLogsID}#/explorer/${savedVisualizationId}`); }; const onSideBarClick = () => { @@ -114,9 +117,19 @@ export const Home = ({ }, 300); }; + useEffect(() => { + chrome.setBreadcrumbs([ + parentBreadcrumb, + { + text: 'Metrics', + href: `#/`, + }, + ]); + }, []); + useEffect(() => { if (!editMode) { - selectedMetrics.length > 0 ? setIsTopPanelDisabled(false) : setIsTopPanelDisabled(true); + selectedMetrics.length > 0 ? setIsTopPanelDisabled(false) : setIsTopPanelDisabled(true); // eslint-disable-line } else { setIsTopPanelDisabled(true); } @@ -145,81 +158,83 @@ export const Home = ({ side={toastRightSide ? 'right' : 'left'} toastLifeTimeMs={6000} /> - ( -
- - - -
-
-
- {!isSidebarClosed && ( - + + ( +
+ + + +
+
+
+ {!isSidebarClosed && ( + + )} + onSideBarClick()} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + className="dscCollapsibleSidebar__collapseButton" + /> +
+
+
+ {selectedMetrics.length > 0 ? ( + + ) : ( + )} - onSideBarClick()} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - className="dscCollapsibleSidebar__collapseButton" - />
-
- {selectedMetrics.length > 0 ? ( - - ) : ( - - )} -
-
- - -
- )} - /> + + +
+ )} + /> + ); }; diff --git a/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap b/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap index c4fcb663bf..9232c58180 100644 --- a/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap +++ b/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap @@ -15,7 +15,9 @@ exports[`Search Bar Component Search Side Bar Component with available metrics 1 -
+
-
+
{ }; return ( -
+
- -
+
- -
- -
- -
- -
- - -
- -
- - - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
- +
- -
- - } - aria-label="resolutionField" - className="resolutionSelectText" - disabled={true} - isInvalid={false} - onChange={[Function]} - prepend="Span Interval" - value={1} + - - } - fullWidth={false} - prepend="Span Interval" +
-
- - - -
- - - - -
-
- - + +
@@ -496,635 +203,261 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled in
- -
-
- + + +
+
-
- - -
- + +
+ +
+
+ +
+
+ + } + aria-label="resolutionField" + className="resolutionSelectText" + disabled={true} + isInvalid={false} + onChange={[Function]} + prepend="Span Interval" + value={1} + > + + } + fullWidth={false} + prepend="Span Interval" + > +
+ + + +
+ + + + +
+ -
- -
- - } - > -
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > -
-
- - - -
-
-
-
-
- } - iconType={false} - isCustom={true} - startDateControl={
} - > -
- -
- - -
-
- -
- - -
- + + + +
- - - Save - - - - - + +
+
+
-
- + +
- +
+
+
+
+
+ +
+ + +
+ +
+ + } + > +
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
+
+ + + +
+
+
+
+
+ } + iconType={false} + isCustom={true} + startDateControl={
} + > +
+ +
+ + +
+
+ +
+ + +
+ + + } + delay="regular" + position="bottom" + > + + + + + + + + + +
+
+
+ + +
+ + +
+ + Save + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
+
+ + + + + +
- +
- -
-
+ +
+ + +
+ - -
+
- +
- +
- -
+ - -
- + +
- -
-
- - + + + - -
-
- - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
- -
- -
- -
- -
- - -
- -
- -
-
- - } - aria-label="resolutionField" - className="resolutionSelectText" - disabled={true} - isInvalid={false} - onChange={[Function]} - prepend="Span Interval" - value={1} - > - - } - fullWidth={false} - prepend="Span Interval" - > -
- - - -
- - - - -
- - -
-
- - - - -
- - - @@ -1808,635 +1502,261 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled wit
- -
-
- + + +
+
-
-
- -
- + +
+ +
+ + +
+
+ + } + aria-label="resolutionField" + className="resolutionSelectText" + disabled={true} + isInvalid={false} + onChange={[Function]} + prepend="Span Interval" + value={1} + > + + } + fullWidth={false} + prepend="Span Interval" + > +
+ + + +
+ + + + +
+ -
- -
- - } - > -
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > -
-
- - - -
-
-
-
-
- } - iconType={false} - isCustom={true} - startDateControl={
} - > -
- -
- - -
-
- -
- - -
- + + + +
- - - Save - - - - - + +
+
+
-
- + +
- +
+
+
+
+
+ +
+ + +
+ +
+ + } + > +
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
+
+ + + +
+
+
+
+
+ } + iconType={false} + isCustom={true} + startDateControl={
} + > +
+ +
+ + +
+
+ +
+ + +
+ + + } + delay="regular" + position="bottom" + > + + + + + + + + + +
+
+
+ + +
+ + +
+ + Save + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
+
+ + + + + +
- +
- -
-
+ +
+ + +
+ - -
+
- +
- +
- -
+ - -
- + +
- -
-
- - + + + - -
-
- - - - -
- - - - - -
-
-
-
-
-
-
-
-
-
-
- -
- -
- -
- -
- - -
- -
- -
-
- - } - aria-label="resolutionField" - className="resolutionSelectText" - disabled={false} - isInvalid={false} - onChange={[Function]} - prepend="Span Interval" - value={1} - > - - } - fullWidth={false} - prepend="Span Interval" - > -
- - - -
- - - - -
- - -
-
- - - - -
- -
- -
- - + + +
+
-
- - -
- + +
+ +
+
+ +
+
+ + } + aria-label="resolutionField" + className="resolutionSelectText" + disabled={false} + isInvalid={false} + onChange={[Function]} + prepend="Span Interval" + value={1} + > + + } + fullWidth={false} + prepend="Span Interval" + > +
+ + + +
+ + + + +
+ -
- -
- +
+ + + + +
-
- - - - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > -
-
- - - -
-
-
-
-
- } - iconType={false} - isCustom={true} - startDateControl={
} +
-
- -
- - + + + + +
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+ +
+ + } + > +
+ -
+ + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" > - - - - - - - - + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + + + + + + + +
+
+ + +
+ } + iconType={false} + isCustom={true} + startDateControl={
} + > +
+
- + +
+
+ +
- - -
- - -
+
+
+ - - Save - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="m" +
-
-
- - - - - -
-
- -
-
+ + + + + + + +
+
+
+ + +
+ + +
+ + Save + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > +
+
+ + + + + +
- +
- -
-
+ +
+ + +
+ ([]); const [isSavePanelOpen, setIsSavePanelOpen] = useState(false); const [selectedPanelOptions, setSelectedPanelOptions] = useState< - EuiComboBoxOptionOption[] | undefined + Array> | undefined >([]); // toggle between panel edit mode @@ -159,7 +160,6 @@ export const TopMenu = ({ const handleSavingObjects = async () => { let savedMetricIds = []; - let savedMetricsInPanels = []; try { savedMetricIds = await Promise.all( @@ -172,9 +172,12 @@ export const TopMenu = ({ ); if (metricLayout.metricType === 'prometheusMetric') { - return await savedObjects.createSavedVisualization(updatedMetric); + return OSDSavedVisualizationClient.getInstance().create(updatedMetric); } else { - return await savedObjects.updateSavedVisualizationById({ + return getSavedObjectsClient({ + objectId: metricLayout.id, + objectType: 'savedVisualization', + }).update({ ...updatedMetric, objectId: metricLayout.id, }); @@ -184,7 +187,8 @@ export const TopMenu = ({ } catch (e) { const message = 'Issue in saving metrics'; console.error(message, e); - setToast('Issue in saving metrics', 'danger'); + setToast(message, 'danger'); + return; } setToast('Saved metrics successfully!'); @@ -195,6 +199,7 @@ export const TopMenu = ({ const soPanels = selectedPanelOptions.filter((panel) => uuidRx.test(panel.panel.id)); const opsPanels = selectedPanelOptions.filter((panel) => !uuidRx.test(panel.panel.id)); + dispatch(addMultipleVizToPanels(soPanels, allMetricIds)); const savedMetricsInOpsPanels = await Promise.all( opsPanels.map((panel) => { return http.post(`${CUSTOM_PANELS_API_PREFIX}/visualizations/multiple`, { @@ -205,107 +210,97 @@ export const TopMenu = ({ }); }) ); - - allMetricIds.forEach((metricId) => { - dispatch(addVizToPanels(soPanels, metricId)); - }); } catch (e) { const message = 'Issue in saving metrics to panels'; console.error(message, e); setToast('Issue in saving metrics', 'danger'); } - setToast('Saved metrics to panels successfully!'); + setToast('Saved metrics to Dashboards successfully!'); } }; return ( <> - - - - - - - - - - - -
- setSpanValue(e.target.value)} - append={ - onResolutionChange(e)} - aria-label="resolutionSelect" - /> - } - disabled={IsTopPanelDisabled} - aria-label="resolutionField" + + + + + +
+ setSpanValue(e.target.value)} + append={ + onResolutionChange(e)} + aria-label="resolutionSelect" /> -
-
- - - - - setIsSavePanelOpen(false)} - > - - - - - setIsSavePanelOpen(false)} - data-test-subj="metrics__SaveCancel" - > - Cancel - - - - { - handleSavingObjects().then(() => setIsSavePanelOpen(false)); - }} - data-test-subj="metrics__SaveConfirm" - > - Save - - - - - - -
- - + } + disabled={IsTopPanelDisabled} + aria-label="resolutionField" + /> +
+
+ + + + + setIsSavePanelOpen(false)} + > + + + + + setIsSavePanelOpen(false)} + data-test-subj="metrics__SaveCancel" + > + Cancel + + + + { + handleSavingObjects().then(() => setIsSavePanelOpen(false)); + }} + data-test-subj="metrics__SaveConfirm" + > + Save + + + + + + +
+ {editMode ? ( <> diff --git a/public/components/notebooks/components/__tests__/__snapshots__/note_table.test.tsx.snap b/public/components/notebooks/components/__tests__/__snapshots__/note_table.test.tsx.snap index 4d3fbeeb05..ae60cd7409 100644 --- a/public/components/notebooks/components/__tests__/__snapshots__/note_table.test.tsx.snap +++ b/public/components/notebooks/components/__tests__/__snapshots__/note_table.test.tsx.snap @@ -134,7 +134,7 @@ exports[` spec renders the component 1`] = ` > spec renders the component 1`] = ` > path-0 @@ -504,7 +504,7 @@ exports[` spec renders the component 1`] = ` > path-1 @@ -579,7 +579,7 @@ exports[` spec renders the component 1`] = ` > path-2 @@ -654,7 +654,7 @@ exports[` spec renders the component 1`] = ` > path-3 @@ -729,7 +729,7 @@ exports[` spec renders the component 1`] = ` > path-4 @@ -1025,7 +1025,7 @@ exports[` spec renders the empty component 1`] = ` > spec renders the empty component 1`] = ` { }, ] as Location[]; const expected = [ - '/app/observability-dashboards#/notebooks/GQ5icXwBJCegTOBKO4Um', - '/app/observability-dashboards#/notebooks/clPiPXwBEM7l9gC0xTpA?view=view_both', - `/testBasePath/app/observability-dashboards#/notebooks/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))&view=output_only&security_tenant=global`, - ] as RedirectProps['to'][]; + '/app/observability-notebooks#/GQ5icXwBJCegTOBKO4Um', + '/app/observability-notebooks#/clPiPXwBEM7l9gC0xTpA?view=view_both', + `/testBasePath/app/observability-notebooks#/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))&view=output_only&security_tenant=global`, + ] as Array; expect(locations.map((location) => convertLegacyNotebooksUrl(location))).toEqual(expected); }); }); diff --git a/public/components/notebooks/components/helpers/legacy_route_helpers.ts b/public/components/notebooks/components/helpers/legacy_route_helpers.ts index 8bda04cc59..1201d9f223 100644 --- a/public/components/notebooks/components/helpers/legacy_route_helpers.ts +++ b/public/components/notebooks/components/helpers/legacy_route_helpers.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { observabilityID } from "../../../../../common/constants/shared"; +import { observabilityNotebookID } from '../../../../../common/constants/shared'; -export const convertLegacyNotebooksUrl = (location: Location)=> { - const pathname = location.pathname.replace('notebooks-dashboards', observabilityID); - const hash = `#/notebooks${location.hash.replace(/^#/, '')}${ +export const convertLegacyNotebooksUrl = (location: Location) => { + const pathname = location.pathname.replace('notebooks-dashboards', observabilityNotebookID); + const hash = `${location.hash}${ location.hash.includes('?') ? location.search.replace(/^\?/, '&') : location.search }`; return pathname + hash; diff --git a/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx b/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx index a174da65b8..c8f3ce1869 100644 --- a/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx +++ b/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx @@ -121,8 +121,7 @@ export const generateInContextReport = async ( toggleReportingLoadingModal(true); let baseUrl = location.pathname + - location.hash.replace(/\?view=(view_both|input_only|output_only)/, '') + - '?view=output_only'; + location.hash.replace(/view=(view_both|input_only|output_only)/, 'view=output_only'); // Add selected tenant info to url try { const tenant = await getTenantInfoIfExists(); diff --git a/public/components/notebooks/components/main.tsx b/public/components/notebooks/components/main.tsx index cd70332d28..82625a86cc 100644 --- a/public/components/notebooks/components/main.tsx +++ b/public/components/notebooks/components/main.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ import { EuiGlobalToastList, EuiLink } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; @@ -17,7 +16,6 @@ import { NOTEBOOKS_API_PREFIX, NOTEBOOKS_DOCUMENTATION_URL, } from '../../../../common/constants/notebooks'; -import { ObservabilitySideBar } from '../../common/side_nav'; import { Notebook } from './notebook'; import { NoteTable } from './note_table'; @@ -95,7 +93,7 @@ export class Main extends React.Component { createNotebook = (newNoteName: string) => { if (newNoteName.length >= 50 || newNoteName.length === 0) { this.setToast('Invalid notebook name', 'danger'); - window.location.assign('#/notebooks'); + window.location.assign('#/'); return; } const newNoteObject = { @@ -108,7 +106,7 @@ export class Main extends React.Component { }) .then(async (res) => { this.setToast(`Notebook "${newNoteName}" successfully created!`); - window.location.assign(`#/notebooks/${res}`); + window.location.assign(`#/${res}`); }) .catch((err) => { this.setToast( @@ -123,7 +121,7 @@ export class Main extends React.Component { }; // Renames an existing notebook - renameNotebook = (editedNoteName: string, editedNoteID: string) => { + renameNotebook = (editedNoteName: string, editedNoteID: string): Promise => { if (editedNoteName.length >= 50 || editedNoteName.length === 0) { this.setToast('Invalid notebook name', 'danger'); return; @@ -145,6 +143,7 @@ export class Main extends React.Component { return { data: newData }; }); this.setToast(`Notebook successfully renamed into "${editedNoteName}"`); + return res; }) .catch((err) => { this.setToast( @@ -310,28 +309,26 @@ export class Main extends React.Component { ( - - - + )} /> ( { cloneNotebook={this.cloneNotebook} deleteNotebook={this.deleteNotebook} setToast={this.setToast} - location={this.props.location} - history={this.props.history} + location={props.location} + history={props.history} /> )} /> diff --git a/public/components/notebooks/components/note_table.tsx b/public/components/notebooks/components/note_table.tsx index 3c5da6df8f..97ea434f7d 100644 --- a/public/components/notebooks/components/note_table.tsx +++ b/public/components/notebooks/components/note_table.tsx @@ -74,15 +74,15 @@ export function NoteTable(props: NoteTableProps) { props.parentBreadcrumb, { text: 'Notebooks', - href: '#/notebooks', + href: '#/', }, ]); props.fetchNotebooks(); }, []); useEffect(() => { - const url = window.location.hash.split('/') - if (url[url.length-1] === 'create') { + const url = window.location.hash.split('/'); + if (url[url.length - 1] === 'create') { createNote(); } }, [location]); @@ -125,7 +125,7 @@ export function NoteTable(props: NoteTableProps) { getCustomModal( onCreate, () => { - closeModal() + closeModal(); history.goBack(); }, 'Name', @@ -253,7 +253,7 @@ export function NoteTable(props: NoteTableProps) { sortable: true, truncateText: true, render: (value, record) => ( - {_.truncate(value, { length: 100 })} + {_.truncate(value, { length: 100 })} ), }, { @@ -319,7 +319,7 @@ export function NoteTable(props: NoteTableProps) { - + Create notebook @@ -380,7 +380,8 @@ export function NoteTable(props: NoteTableProps) { - diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index 2d63e61ca8..a68f87c46f 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -75,7 +75,7 @@ type NotebookProps = { http: CoreStart['http']; parentBreadcrumb: ChromeBreadcrumb; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; - renameNotebook: (newNoteName: string, noteId: string) => void; + renameNotebook: (newNoteName: string, noteId: string) => Promise; cloneNotebook: (newNoteName: string, noteId: string) => Promise; deleteNotebook: (noteList: string[], toastMessage?: string) => void; setToast: (title: string, color?: string, text?: string) => void; @@ -285,9 +285,13 @@ export class Notebook extends Component { this.setState({ modalLayout: getCustomModal( (newName: string) => { - this.props.renameNotebook(newName, this.props.openedNoteId); - this.setState({ isModalVisible: false }); - this.loadNotebook(); + this.props.renameNotebook(newName, this.props.openedNoteId).then((res) => { + this.setState({ isModalVisible: false }); + window.location.assign(`#/${res.message.objectId}`); + setTimeout(() => { + this.loadNotebook(); + }, 300); + }); }, () => this.setState({ isModalVisible: false }), 'Name', @@ -306,7 +310,7 @@ export class Notebook extends Component { modalLayout: getCustomModal( (newName: string) => { this.props.cloneNotebook(newName, this.props.openedNoteId).then((id: string) => { - window.location.assign(`#/notebooks/${id}`); + window.location.assign(`#/${id}`); setTimeout(() => { this.loadNotebook(); }, 300); @@ -631,11 +635,11 @@ export class Notebook extends Component { this.props.parentBreadcrumb, { text: 'Notebooks', - href: '#/notebooks', + href: '#/', }, { text: path, - href: `#/notebooks/${this.props.openedNoteId}`, + href: `#/${this.props.openedNoteId}`, }, ]); } diff --git a/public/components/notebooks/components/paragraph_components/para_input.tsx b/public/components/notebooks/components/paragraph_components/para_input.tsx index 5cc41ddd0d..b0d2ae12a4 100644 --- a/public/components/notebooks/components/paragraph_components/para_input.tsx +++ b/public/components/notebooks/components/paragraph_components/para_input.tsx @@ -31,6 +31,7 @@ import { Input, Prompt } from '@nteract/presentational-components'; import { uiSettingsService } from '../../../../../common/utils'; import React, { useState } from 'react'; import { ParaType } from '../../../../../common/types/notebooks'; +import { observabilityLogsID } from '../../../../../common/constants/shared'; /* * "ParaInput" component is used by notebook to populate paragraph inputs for an open notebook. @@ -118,7 +119,7 @@ export const ParaInput = (props: { const renderOption = (option: EuiComboBoxOptionOption, searchValue: string) => { let visURL = `visualize#/edit/${option.key}?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'${props.startTime}',to:'${props.endTime}'))`; if (option.className === 'OBSERVABILITY_VISUALIZATION') { - visURL = `#/event_analytics/explorer/${option.key}`; + visURL = `${observabilityLogsID}#/explorer/${option.key}`; } return ( diff --git a/public/components/notebooks/components/paragraph_components/para_output.tsx b/public/components/notebooks/components/paragraph_components/para_output.tsx index 0a3776e9fc..f1848872e7 100644 --- a/public/components/notebooks/components/paragraph_components/para_output.tsx +++ b/public/components/notebooks/components/paragraph_components/para_output.tsx @@ -134,7 +134,7 @@ export const ParaOutput = (props: { fromObs = fromObs === 'Invalid date' ? visInput.timeRange.from : fromObs; toObs = toObs === 'Invalid date' ? visInput.timeRange.to : toObs; const onEditClick = (savedVisualizationId: string) => { - window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); + window.location.assign(`observability-logs#/explorer/${savedVisualizationId}`); }; return ( <> @@ -161,6 +161,7 @@ export const ParaOutput = (props: { case 'HTML': return ( + {/* eslint-disable-next-line react/jsx-pascal-case */} ); diff --git a/public/components/trace_analytics/components/common/legacy_route_helpers.ts b/public/components/trace_analytics/components/common/legacy_route_helpers.ts index 139456bf58..3be7bfbf0c 100644 --- a/public/components/trace_analytics/components/common/legacy_route_helpers.ts +++ b/public/components/trace_analytics/components/common/legacy_route_helpers.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { observabilityID } from "../../../../../common/constants/shared"; +import { observabilityTracesID } from '../../../../../common/constants/shared'; -export const convertLegacyTraceAnalyticsUrl = (location: Location)=> { - const pathname = location.pathname.replace('trace-analytics-dashboards', observabilityID); - const hash = `#/trace_analytics${location.hash.replace(/^#/, '/home')}${ +export const convertLegacyTraceAnalyticsUrl = (location: Location) => { + const pathname = location.pathname.replace('trace-analytics-dashboards', observabilityTracesID); + const hash = `${location.hash}${ location.hash.includes('?') ? location.search.replace(/^\?/, '&') : location.search }`; return pathname + hash; diff --git a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap b/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap index f7e00be9bf..8d437c4510 100644 --- a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap +++ b/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap @@ -62,10 +62,7 @@ exports[`Dashboard component renders dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -334,10 +331,7 @@ exports[`Dashboard component renders dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -2426,10 +2420,7 @@ exports[`Dashboard component renders empty dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -2693,10 +2684,7 @@ exports[`Dashboard component renders empty dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -4758,10 +4746,7 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -5031,10 +5016,7 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", diff --git a/public/components/trace_analytics/components/dashboard/dashboard_content.tsx b/public/components/trace_analytics/components/dashboard/dashboard_content.tsx index 2cc0af94bd..9cf1d8cd9e 100644 --- a/public/components/trace_analytics/components/dashboard/dashboard_content.tsx +++ b/public/components/trace_analytics/components/dashboard/dashboard_content.tsx @@ -44,7 +44,7 @@ export function DashboardContent(props: DashboardProps) { startTime, endTime, childBreadcrumbs, - parentBreadcrumbs, + parentBreadcrumb, filters, setStartTime, setEndTime, @@ -73,13 +73,17 @@ export function DashboardContent(props: DashboardProps) { useEffect(() => { if (showTimeoutToast === true && toasts.length === 0) { - setToast!('Query took too long to execute.', 'danger', 'Reduce time range or filter your data. If issue persists, consider increasing your cluster size.') + setToast!( + 'Query took too long to execute.', + 'danger', + 'Reduce time range or filter your data. If issue persists, consider increasing your cluster size.' + ); } setShowTimeoutToast(false); - }, [showTimeoutToast]) + }, [showTimeoutToast]); useEffect(() => { - chrome.setBreadcrumbs([...parentBreadcrumbs, ...childBreadcrumbs]); + chrome.setBreadcrumbs([parentBreadcrumb, ...childBreadcrumbs]); const validFilters = getValidFilterFields(mode, page); setFilters([ ...filters.map((filter) => ({ @@ -93,7 +97,7 @@ export function DashboardContent(props: DashboardProps) { useEffect(() => { let newFilteredService = ''; for (const filter of filters) { - if (mode === 'data_prepper') { + if (mode === 'data_prepper') { if (filter.field === 'serviceName') { newFilteredService = filter.value; break; @@ -106,8 +110,22 @@ export function DashboardContent(props: DashboardProps) { } } setFilteredService(newFilteredService); - if (!redirect && ((mode === 'data_prepper' && dataPrepperIndicesExist) || (mode === 'jaeger' && jaegerIndicesExist))) refresh(newFilteredService); - }, [filters, startTime, endTime, appConfigs, redirect, mode, dataPrepperIndicesExist, jaegerIndicesExist]); + if ( + !redirect && + ((mode === 'data_prepper' && dataPrepperIndicesExist) || + (mode === 'jaeger' && jaegerIndicesExist)) + ) + refresh(newFilteredService); + }, [ + filters, + startTime, + endTime, + appConfigs, + redirect, + mode, + dataPrepperIndicesExist, + jaegerIndicesExist, + ]); const refresh = async (currService?: string) => { setLoading(true); @@ -154,12 +172,12 @@ export function DashboardContent(props: DashboardProps) { mode, () => setShowTimeoutToast(true), // () => { - // if (toasts.length === 0) { + // if (toasts.length === 0) { // setToast!('Query took too long to execute.', 'danger', 'Reduce time range or filter your data. If issue persists, consider increasing your cluster size.'); // } // }, setPercentileMap - ).finally(() => setLoading(false)) + ).finally(() => setLoading(false)); handleJaegerErrorDashboardRequest( http, DSL, @@ -170,7 +188,7 @@ export function DashboardContent(props: DashboardProps) { mode, () => setShowTimeoutToast(true), // () => { - // if (toasts.length === 0) { + // if (toasts.length === 0) { // setToast!('Query took too long to execute.', 'danger', 'Reduce time range or filter your data. If issue persists, consider increasing your cluster size.'); // } // }, @@ -295,7 +313,8 @@ export function DashboardContent(props: DashboardProps) { mode={mode} /> - {((mode === 'data_prepper' && dataPrepperIndicesExist) || mode === 'jaeger' && jaegerIndicesExist) ? ( + {(mode === 'data_prepper' && dataPrepperIndicesExist) || + (mode === 'jaeger' && jaegerIndicesExist) ? (
{mode === 'data_prepper' ? ( <> @@ -359,7 +378,7 @@ export function DashboardContent(props: DashboardProps) { )}
) : ( - + )} ); diff --git a/public/components/trace_analytics/components/dashboard/dashboard_table.tsx b/public/components/trace_analytics/components/dashboard/dashboard_table.tsx index 47063a0387..ecc8872030 100644 --- a/public/components/trace_analytics/components/dashboard/dashboard_table.tsx +++ b/public/components/trace_analytics/components/dashboard/dashboard_table.tsx @@ -152,6 +152,7 @@ export function DashboardTable(props: { ), align: 'center', + // eslint-disable-next-line @typescript-eslint/naming-convention sortable: ({ dashboard_latency_variance }) => dashboard_latency_variance?.length > 0 ? dashboard_latency_variance[2] - dashboard_latency_variance[0] @@ -336,7 +337,7 @@ export function DashboardTable(props: { }); if (props.page !== 'app') { props.setRedirect(true); - location.assign('#/trace_analytics/traces'); + location.assign('#/traces'); } }} > diff --git a/public/components/trace_analytics/components/dashboard/top_error_rates_table.tsx b/public/components/trace_analytics/components/dashboard/top_error_rates_table.tsx index 548d6c1719..6f5b32b5e6 100644 --- a/public/components/trace_analytics/components/dashboard/top_error_rates_table.tsx +++ b/public/components/trace_analytics/components/dashboard/top_error_rates_table.tsx @@ -206,7 +206,7 @@ export function ErrorRatesTable(props: { ]); if (props.page !== 'app') { props.setRedirect(true); - location.assign('#/trace_analytics/traces'); + location.assign('#/traces'); } }} > diff --git a/public/components/trace_analytics/components/dashboard/top_latency_table.tsx b/public/components/trace_analytics/components/dashboard/top_latency_table.tsx index 5635b0beb5..0cc290acb7 100644 --- a/public/components/trace_analytics/components/dashboard/top_latency_table.tsx +++ b/public/components/trace_analytics/components/dashboard/top_latency_table.tsx @@ -162,6 +162,7 @@ export function LatencyTable(props: { ), align: 'center', + // eslint-disable-next-line @typescript-eslint/naming-convention sortable: ({ dashboard_latency_variance }) => dashboard_latency_variance?.length > 0 ? dashboard_latency_variance[2] - dashboard_latency_variance[0] @@ -320,7 +321,7 @@ export function LatencyTable(props: { ]); if (props.page !== 'app') { props.setRedirect(true); - location.assign('#/trace_analytics/traces'); + location.assign('#/traces'); } }} > diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap index 25deb6fbc7..0d88b21395 100644 --- a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap +++ b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap @@ -109,13 +109,11 @@ exports[`Services component renders empty services page 1`] = ` } nameColumnAction={[Function]} page="services" - parentBreadcrumbs={ - Array [ - Object { - "href": "test#/", - "text": "test", - }, - ] + parentBreadcrumb={ + Object { + "href": "test#/", + "text": "test", + } } query="" setEndTime={[MockFunction]} @@ -378,13 +376,11 @@ exports[`Services component renders empty services page 1`] = ` } nameColumnAction={[Function]} page="services" - parentBreadcrumbs={ - Array [ - Object { - "href": "test#/", - "text": "test", - }, - ] + parentBreadcrumb={ + Object { + "href": "test#/", + "text": "test", + } } query="" setEndTime={[MockFunction]} @@ -1957,10 +1953,7 @@ exports[`Services component renders jaeger services page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -2232,10 +2225,7 @@ exports[`Services component renders jaeger services page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -3361,10 +3351,7 @@ exports[`Services component renders services page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -3635,10 +3622,7 @@ exports[`Services component renders services page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", diff --git a/public/components/trace_analytics/components/services/__tests__/services.test.tsx b/public/components/trace_analytics/components/services/__tests__/services.test.tsx index d427239da4..5dca84b918 100644 --- a/public/components/trace_analytics/components/services/__tests__/services.test.tsx +++ b/public/components/trace_analytics/components/services/__tests__/services.test.tsx @@ -39,7 +39,7 @@ describe('Services component', () => { { endTime="now" setEndTime={setEndTime} page="services" - mode='data_prepper' + mode="data_prepper" dataPrepperIndicesExist={true} modes={modes} /> @@ -99,7 +99,7 @@ describe('Services component', () => { endTime="now" setEndTime={setEndTime} page="services" - mode='data_prepper' + mode="data_prepper" dataPrepperIndicesExist={true} modes={modes} /> @@ -145,7 +145,7 @@ describe('Services component', () => { endTime="now" setEndTime={setEndTime} page="services" - mode='jaeger' + mode="jaeger" dataPrepperIndicesExist={false} jaegerIndicesExist={true} modes={modes} diff --git a/public/components/trace_analytics/components/services/service_view.tsx b/public/components/trace_analytics/components/services/service_view.tsx index 0121af258e..ac4875526d 100644 --- a/public/components/trace_analytics/components/services/service_view.tsx +++ b/public/components/trace_analytics/components/services/service_view.tsx @@ -62,18 +62,18 @@ export function ServiceView(props: ServiceViewProps) { useEffect(() => { props.chrome.setBreadcrumbs([ - ...props.parentBreadcrumbs, + props.parentBreadcrumb, { text: 'Trace analytics', - href: '#/trace_analytics/home', + href: '#/home', }, { text: 'Services', - href: '#/trace_analytics/services', + href: '#/services', }, { text: props.serviceName, - href: `#/trace_analytics/services/${encodeURIComponent(props.serviceName)}`, + href: `#/services/${encodeURIComponent(props.serviceName)}`, }, ]); }, [props.serviceName]); @@ -138,7 +138,7 @@ export function ServiceView(props: ServiceViewProps) { {fields.connected_services && fields.connected_services.length ? fields.connected_services .map((service: string) => ( - + {service} )) @@ -193,7 +193,7 @@ export function ServiceView(props: ServiceViewProps) { inverted: false, disabled: false, }); - location.assign('#/trace_analytics/traces'); + location.assign('#/traces'); }} > diff --git a/public/components/trace_analytics/components/services/services_content.tsx b/public/components/trace_analytics/components/services/services_content.tsx index e25bf8726f..417c9420e0 100644 --- a/public/components/trace_analytics/components/services/services_content.tsx +++ b/public/components/trace_analytics/components/services/services_content.tsx @@ -30,7 +30,7 @@ export function ServicesContent(props: ServicesProps) { endTime, appConfigs = [], childBreadcrumbs, - parentBreadcrumbs, + parentBreadcrumb, nameColumnAction, traceColumnAction, setFilters, @@ -51,7 +51,7 @@ export function ServicesContent(props: ServicesProps) { const [filteredService, setFilteredService] = useState(''); useEffect(() => { - chrome.setBreadcrumbs([...parentBreadcrumbs, ...childBreadcrumbs]); + chrome.setBreadcrumbs([parentBreadcrumb, ...childBreadcrumbs]); const validFilters = getValidFilterFields(mode, 'services'); setFilters([ ...filters.map((filter) => ({ @@ -71,12 +71,25 @@ export function ServicesContent(props: ServicesProps) { } } setFilteredService(newFilteredService); - if (!redirect && ((mode === 'data_prepper' && dataPrepperIndicesExist) || (mode === 'jaeger' && jaegerIndicesExist))) refresh(newFilteredService); + if ( + !redirect && + ((mode === 'data_prepper' && dataPrepperIndicesExist) || + (mode === 'jaeger' && jaegerIndicesExist)) + ) + refresh(newFilteredService); }, [filters, appConfigs, redirect, mode, jaegerIndicesExist, dataPrepperIndicesExist]); const refresh = async (currService?: string) => { setLoading(true); - const DSL = filtersToDsl(mode, filters, query,processTimeStamp(startTime, mode), processTimeStamp(endTime, mode), page, appConfigs); + const DSL = filtersToDsl( + mode, + filters, + query, + processTimeStamp(startTime, mode), + processTimeStamp(endTime, mode), + page, + appConfigs + ); // service map should not be filtered by service name const serviceMapDSL = _.cloneDeep(DSL); serviceMapDSL.query.bool.must = serviceMapDSL.query.bool.must.filter( @@ -84,7 +97,13 @@ export function ServicesContent(props: ServicesProps) { ); await Promise.all([ handleServicesRequest(http, DSL, setTableItems, mode), - handleServiceMapRequest(http, serviceMapDSL, mode, setServiceMap, currService || filteredService), + handleServiceMapRequest( + http, + serviceMapDSL, + mode, + setServiceMap, + currService || filteredService + ), ]); setLoading(false); }; @@ -133,7 +152,7 @@ export function ServicesContent(props: ServicesProps) { dataPrepperIndicesExist={dataPrepperIndicesExist} /> - { (mode === 'data_prepper' && dataPrepperIndicesExist) ? + {mode === 'data_prepper' && dataPrepperIndicesExist ? ( : (
) - } + /> + ) : ( +
+ )} ); } diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap index e0acd2e97a..c39db34c17 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap @@ -108,13 +108,11 @@ exports[`Traces component renders empty traces page 1`] = ` ] } page="traces" - parentBreadcrumbs={ - Array [ - Object { - "href": "test#/", - "text": "test", - }, - ] + parentBreadcrumb={ + Object { + "href": "test#/", + "text": "test", + } } query="" setEndTime={[MockFunction]} @@ -376,13 +374,11 @@ exports[`Traces component renders empty traces page 1`] = ` ] } page="traces" - parentBreadcrumbs={ - Array [ - Object { - "href": "test#/", - "text": "test", - }, - ] + parentBreadcrumb={ + Object { + "href": "test#/", + "text": "test", + } } query="" setEndTime={[MockFunction]} @@ -1423,10 +1419,7 @@ exports[`Traces component renders jaeger traces page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -1697,10 +1690,7 @@ exports[`Traces component renders jaeger traces page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -2815,10 +2805,7 @@ exports[`Traces component renders traces page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", @@ -3088,10 +3075,7 @@ exports[`Traces component renders traces page 1`] = ` "calls": Array [ Array [ Array [ - Object { - "href": "test#/", - "text": "test", - }, + undefined, Object { "href": "#/trace_analytics/home", "text": "Trace analytics", diff --git a/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx b/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx index 8128b99be8..cfd56c99f1 100644 --- a/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx +++ b/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx @@ -38,7 +38,7 @@ describe('Traces component', () => { { endTime="now" setEndTime={setEndTime} page="traces" - mode='data_prepper' + mode="data_prepper" dataPrepperIndicesExist={true} modes={modes} /> @@ -94,7 +94,7 @@ describe('Traces component', () => { setStartTime={setStartTime} endTime="now" setEndTime={setEndTime} - mode='data_prepper' + mode="data_prepper" page="traces" modes={modes} dataPrepperIndicesExist={true} @@ -138,7 +138,7 @@ describe('Traces component', () => { setStartTime={setStartTime} endTime="now" setEndTime={setEndTime} - mode='jaeger' + mode="jaeger" page="traces" modes={modes} dataPrepperIndicesExist={false} diff --git a/public/components/trace_analytics/components/traces/trace_view.tsx b/public/components/trace_analytics/components/traces/trace_view.tsx index 46fd003253..c150a19ff5 100644 --- a/public/components/trace_analytics/components/traces/trace_view.tsx +++ b/public/components/trace_analytics/components/traces/trace_view.tsx @@ -206,18 +206,18 @@ export function TraceView(props: TraceViewProps) { useEffect(() => { props.chrome.setBreadcrumbs([ - ...props.parentBreadcrumbs, + props.parentBreadcrumb, { text: 'Trace analytics', - href: '#/trace_analytics/home', + href: '#/home', }, { text: 'Traces', - href: '#/trace_analytics/traces', + href: '#/traces', }, { text: props.traceId, - href: `#/trace_analytics/traces/${encodeURIComponent(props.traceId)}`, + href: `#/traces/${encodeURIComponent(props.traceId)}`, }, ]); refresh(); diff --git a/public/components/trace_analytics/components/traces/traces_content.tsx b/public/components/trace_analytics/components/traces/traces_content.tsx index 66764f9883..2efc70ec76 100644 --- a/public/components/trace_analytics/components/traces/traces_content.tsx +++ b/public/components/trace_analytics/components/traces/traces_content.tsx @@ -23,7 +23,7 @@ export function TracesContent(props: TracesProps) { appConfigs, startTime, endTime, - parentBreadcrumbs, + parentBreadcrumb, childBreadcrumbs, traceIdColumnAction, setQuery, @@ -39,7 +39,7 @@ export function TracesContent(props: TracesProps) { const [loading, setLoading] = useState(false); useEffect(() => { - chrome.setBreadcrumbs([...parentBreadcrumbs, ...childBreadcrumbs]); + chrome.setBreadcrumbs([parentBreadcrumb, ...childBreadcrumbs]); const validFilters = getValidFilterFields(mode, 'traces'); setFilters([ ...filters.map((filter) => ({ @@ -51,13 +51,33 @@ export function TracesContent(props: TracesProps) { }, []); useEffect(() => { - if (!redirect && ((mode === 'data_prepper' && dataPrepperIndicesExist) || (mode === 'jaeger' && jaegerIndicesExist))) refresh(); + if ( + !redirect && + ((mode === 'data_prepper' && dataPrepperIndicesExist) || + (mode === 'jaeger' && jaegerIndicesExist)) + ) + refresh(); }, [filters, appConfigs, redirect, mode, dataPrepperIndicesExist, jaegerIndicesExist]); const refresh = async (sort?: PropertySort) => { setLoading(true); - const DSL = filtersToDsl(mode, filters, query, processTimeStamp(startTime, mode), processTimeStamp(endTime, mode), page, appConfigs); - const timeFilterDSL = filtersToDsl(mode, [], '', processTimeStamp(startTime, mode), processTimeStamp(endTime, mode), page); + const DSL = filtersToDsl( + mode, + filters, + query, + processTimeStamp(startTime, mode), + processTimeStamp(endTime, mode), + page, + appConfigs + ); + const timeFilterDSL = filtersToDsl( + mode, + [], + '', + processTimeStamp(startTime, mode), + processTimeStamp(endTime, mode), + page + ); await handleTracesRequest(http, DSL, timeFilterDSL, tableItems, setTableItems, mode, sort); setLoading(false); }; diff --git a/public/components/trace_analytics/home.tsx b/public/components/trace_analytics/home.tsx index d9c443ff15..7d4569949b 100644 --- a/public/components/trace_analytics/home.tsx +++ b/public/components/trace_analytics/home.tsx @@ -6,36 +6,35 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import React, { ReactChild, useEffect, useState } from 'react'; -import { Route, RouteComponentProps } from 'react-router-dom'; -import { - ChromeBreadcrumb, - ChromeStart, - HttpStart, -} from '../../../../../src/core/public'; -import { ObservabilitySideBar } from '../common/side_nav'; +import { HashRouter, Route, RouteComponentProps } from 'react-router-dom'; +import { ChromeBreadcrumb, ChromeStart, HttpStart } from '../../../../../src/core/public'; import { FilterType } from './components/common/filters/filters'; import { SearchBarProps } from './components/common/search_bar'; import { Dashboard } from './components/dashboard'; import { Services, ServiceView } from './components/services'; import { Traces, TraceView } from './components/traces'; -import { handleDataPrepperIndicesExistRequest, handleJaegerIndicesExistRequest } from './requests/request_handler'; +import { + handleDataPrepperIndicesExistRequest, + handleJaegerIndicesExistRequest, +} from './requests/request_handler'; +import { TraceSideBar } from './trace_side_nav'; export interface TraceAnalyticsCoreDeps { - parentBreadcrumbs: ChromeBreadcrumb[]; + parentBreadcrumb: ChromeBreadcrumb; http: HttpStart; chrome: ChromeStart; } interface HomeProps extends RouteComponentProps, TraceAnalyticsCoreDeps {} -export type TraceAnalyticsMode = 'jaeger' | 'data_prepper' +export type TraceAnalyticsMode = 'jaeger' | 'data_prepper'; export interface TraceAnalyticsComponentDeps extends TraceAnalyticsCoreDeps, SearchBarProps { mode: TraceAnalyticsMode; - modes: { + modes: Array<{ id: string; title: string; - }[]; + }>; setMode: (mode: TraceAnalyticsMode) => void; jaegerIndicesExist: boolean; dataPrepperIndicesExist: boolean; @@ -44,7 +43,9 @@ export interface TraceAnalyticsComponentDeps extends TraceAnalyticsCoreDeps, Sea export const Home = (props: HomeProps) => { const [dataPrepperIndicesExist, setDataPrepperIndicesExist] = useState(false); const [jaegerIndicesExist, setJaegerIndicesExist] = useState(false); - const [mode, setMode] = useState(sessionStorage.getItem('TraceAnalyticsMode') as TraceAnalyticsMode || 'jaeger') + const [mode, setMode] = useState( + (sessionStorage.getItem('TraceAnalyticsMode') as TraceAnalyticsMode) || 'jaeger' + ); const storedFilters = sessionStorage.getItem('TraceAnalyticsFilters'); const [query, setQuery] = useState(sessionStorage.getItem('TraceAnalyticsQuery') || ''); const [filters, setFilters] = useState( @@ -81,18 +82,17 @@ export const Home = (props: HomeProps) => { }; useEffect(() => { - handleDataPrepperIndicesExistRequest(props.http, setDataPrepperIndicesExist) + handleDataPrepperIndicesExistRequest(props.http, setDataPrepperIndicesExist); handleJaegerIndicesExistRequest(props.http, setJaegerIndicesExist); }, []); - const modes = [ { id: 'jaeger', title: 'Jaeger', 'data-test-subj': 'jaeger-mode' }, { id: 'data_prepper', title: 'Data Prepper', 'data-test-subj': 'data-prepper-mode' }, ]; useEffect(() => { - if (!sessionStorage.getItem('TraceAnalyticsMode')){ + if (!sessionStorage.getItem('TraceAnalyticsMode')) { if (dataPrepperIndicesExist) { setMode('data_prepper'); } else if (jaegerIndicesExist) { @@ -104,54 +104,53 @@ export const Home = (props: HomeProps) => { const dashboardBreadcrumbs = [ { text: 'Trace analytics', - href: '#/trace_analytics/home', + href: '#/', }, { text: 'Dashboard', - href: '#/trace_analytics/home', + href: '#/', }, ]; const serviceBreadcrumbs = [ { text: 'Trace analytics', - href: '#/trace_analytics/home', + href: '#/', }, { text: 'Services', - href: '#/trace_analytics/services', + href: '#/services', }, ]; const traceBreadcrumbs = [ { text: 'Trace analytics', - href: '#/trace_analytics/home', + href: '#/', }, { text: 'Traces', - href: '#/trace_analytics/traces', + href: '#/traces', }, ]; - const nameColumnAction = (item: any) => - location.assign(`#/trace_analytics/services/${encodeURIComponent(item)}`); + const nameColumnAction = (item: any) => location.assign(`#/services/${encodeURIComponent(item)}`); - const traceColumnAction = () => location.assign('#/trace_analytics/traces'); + const traceColumnAction = () => location.assign('#/traces'); const traceIdColumnAction = (item: any) => - location.assign(`#/trace_analytics/traces/${encodeURIComponent(item)}`); + location.assign(`#/traces/${encodeURIComponent(item)}`); const [appConfigs, _] = useState([]); const commonProps: TraceAnalyticsComponentDeps = { - parentBreadcrumbs: props.parentBreadcrumbs, + parentBreadcrumb: props.parentBreadcrumb, http: props.http, chrome: props.chrome, query, setQuery: setQueryWithStorage, filters, - appConfigs: appConfigs, + appConfigs, setFilters: setFiltersWithStorage, startTime, setStartTime: setStartTimeWithStorage, @@ -159,7 +158,9 @@ export const Home = (props: HomeProps) => { setEndTime: setEndTimeWithStorage, mode, modes, - setMode: (mode: TraceAnalyticsMode) => {setMode(mode)}, + setMode: (traceMode: TraceAnalyticsMode) => { + setMode(traceMode); + }, jaegerIndicesExist, dataPrepperIndicesExist, }; @@ -167,84 +168,92 @@ export const Home = (props: HomeProps) => { return ( <> { - setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); - }} - toastLifeTimeMs={6000} - /> - ( - - - - )} + toasts={toasts} + dismissToast={(removedToast) => { + setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); + }} + toastLifeTimeMs={6000} /> - ( - - + ( + + + + )} + /> + ( + + + + )} + /> + ( + - - )} - /> - ( - - )} - /> - ( - - + ( + + + + )} + /> + ( + - - )} - /> - ( - { - for (const addedFilter of filters) { - if ( - addedFilter.field === filter.field && - addedFilter.operator === filter.operator && - addedFilter.value === filter.value - ) { - return; + 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]; - setFiltersWithStorage(newFilters); - }} - /> - )} - /> + const newFilters = [...filters, filter]; + setFiltersWithStorage(newFilters); + }} + /> + )} + /> + ); }; diff --git a/public/components/trace_analytics/requests/dashboard_request_handler.ts b/public/components/trace_analytics/requests/dashboard_request_handler.ts index ac29cc9b2d..d25ad03fde 100644 --- a/public/components/trace_analytics/requests/dashboard_request_handler.ts +++ b/public/components/trace_analytics/requests/dashboard_request_handler.ts @@ -191,10 +191,7 @@ export const handleJaegerDashboardRequest = async ( return map; }) .catch((error) => { - console.log("error here") - console.error(error) - - setToast('hello') + console.error(error); }); await handleDslRequest(http, DSL, getJaegerDashboardQuery(), mode, true, setShowTimeoutToast) diff --git a/public/components/trace_analytics/trace_side_nav.tsx b/public/components/trace_analytics/trace_side_nav.tsx new file mode 100644 index 0000000000..77cb44596d --- /dev/null +++ b/public/components/trace_analytics/trace_side_nav.tsx @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageSideBar, + EuiSideNav, + EuiSideNavItemType, + EuiSwitch, +} from '@elastic/eui'; +import React from 'react'; + +export function TraceSideBar(props: { children: React.ReactNode }) { + function setIsSelected(items: Array>, hash: string): boolean { + if (hash === '#/') { + items[0].isSelected = true; + return true; + } + if (hash === '#/traces') { + items[0].items[0].isSelected = true; + return true; + } + if (hash === '#/services') { + items[0].items[1].isSelected = true; + return true; + } + } + + const items = [ + { + name: 'Trace analytics', + id: 1, + href: '#/', + items: [ + { + name: 'Traces', + id: 1.1, + href: '#/traces', + }, + { + name: 'Services', + id: 1.2, + href: '#/services', + }, + ], + }, + ]; + + setIsSelected(items, location.hash); + + return ( + + + + + + + + + {props.children} + + ); +} diff --git a/public/embeddable/observability_embeddable_factory.tsx b/public/embeddable/observability_embeddable_factory.tsx index 53e7b7859f..5392881567 100644 --- a/public/embeddable/observability_embeddable_factory.tsx +++ b/public/embeddable/observability_embeddable_factory.tsx @@ -20,7 +20,7 @@ import { OnSaveProps, SavedObjectMetaData, } from '../../../../src/plugins/saved_objects/public'; -import { observabilityID } from '../../common/constants/shared'; +import { observabilityID, observabilityLogsID } from '../../common/constants/shared'; import { VisualizationSavedObjectAttributes, VISUALIZATION_SAVED_OBJECT, @@ -63,8 +63,8 @@ export class ObservabilityEmbeddableFactoryDefinition input: SavedObjectEmbeddableInput, parent?: IContainer ) { - const editPath = `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${savedObjectId}`; - const editUrl = `/app/${observabilityID}${editPath}`; + const editPath = `#/explorer/${VISUALIZATION_SAVED_OBJECT}:${savedObjectId}`; + const editUrl = `/app/${observabilityLogsID}${editPath}`; return new ObservabilityEmbeddable( { editUrl, diff --git a/public/framework/core_refs.ts b/public/framework/core_refs.ts new file mode 100644 index 0000000000..dd2367f19a --- /dev/null +++ b/public/framework/core_refs.ts @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { HttpStart, IToasts } from '../../../../src/core/public'; +import { SavedObjectsClientContract } from '../../../../src/core/public'; +import PPLService from '../services/requests/ppl'; + +class CoreRefs { + private static _instance: CoreRefs; + + public http?: HttpStart; + public savedObjectsClient?: SavedObjectsClientContract; + public pplService?: PPLService; + public toasts?: IToasts; + private constructor() { + // ... + } + + public static get Instance() { + // Do you need arguments? Make it a regular static method instead. + return this._instance || (this._instance = new this()); + } +} + +export const coreRefs = CoreRefs.Instance; diff --git a/public/framework/redux/reducers/index.ts b/public/framework/redux/reducers/index.ts index c602dbc97a..d392b8265d 100644 --- a/public/framework/redux/reducers/index.ts +++ b/public/framework/redux/reducers/index.ts @@ -14,6 +14,7 @@ import { explorerVisualizationReducer } from '../../../components/event_analytic import { explorerVisualizationConfigReducer } from '../../../components/event_analytics/redux/slices/viualization_config_slice'; import { patternsReducer } from '../../../components/event_analytics/redux/slices/patterns_slice'; import { metricsReducers } from '../../../components/metrics/redux/slices/metrics_slice'; +import { panelReducer } from '../../../components/custom_panels/redux/panel_slice'; const combinedReducer = combineReducers({ // explorer reducers @@ -26,6 +27,7 @@ const combinedReducer = combineReducers({ explorerVisualizationConfig: explorerVisualizationConfigReducer, patterns: patternsReducer, metrics: metricsReducers, + customPanel: panelReducer, }); export type RootState = ReturnType; diff --git a/public/index.ts b/public/index.ts index 97037a9f7d..a43ed0e205 100644 --- a/public/index.ts +++ b/public/index.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import './variables.scss'; import './components/trace_analytics/index.scss'; import './components/notebooks/index.scss' diff --git a/public/plugin.ts b/public/plugin.ts index e9b2815d65..0a94c6273c 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -3,12 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import './index.scss'; + +import { i18n } from '@osd/i18n'; +import { + AppCategory, + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, +} from '../../../src/core/public'; import { CREATE_TAB_PARAM, CREATE_TAB_PARAM_KEY, TAB_CHART_ID } from '../common/constants/explorer'; + import { - observabilityID, + observabilityApplicationsID, + observabilityApplicationsPluginOrder, + observabilityApplicationsTitle, + observabilityTracesTitle, + observabilityMetricsID, + observabilityMetricsPluginOrder, + observabilityMetricsTitle, + observabilityNotebookID, + observabilityNotebookPluginOrder, + observabilityNotebookTitle, + observabilityTracesID, + observabilityTracesPluginOrder, + observabilityPanelsID, + observabilityPanelsTitle, + observabilityPanelsPluginOrder, + observabilityLogsID, + observabilityLogsTitle, + observabilityLogsPluginOrder, observabilityPluginOrder, - observabilityTitle, } from '../common/constants/shared'; import { QueryManager } from '../common/query_manager'; import { VISUALIZATION_SAVED_OBJECT } from '../common/types/observability_saved_object_attributes'; @@ -20,6 +46,9 @@ import { } from '../common/utils'; import { convertLegacyNotebooksUrl } from './components/notebooks/components/helpers/legacy_route_helpers'; import { convertLegacyTraceAnalyticsUrl } from './components/trace_analytics/components/common/legacy_route_helpers'; +import { SavedObject } from '../../../src/core/public'; +import { coreRefs } from './framework/core_refs'; + import { OBSERVABILITY_EMBEDDABLE, OBSERVABILITY_EMBEDDABLE_DESCRIPTION, @@ -66,34 +95,96 @@ export class ObservabilityPlugin window.location.assign(convertLegacyTraceAnalyticsUrl(window.location)); } - core.application.register({ - id: observabilityID, - title: observabilityTitle, - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: observabilityPluginOrder, - async mount(params: AppMountParameters) { - const { Observability } = await import('./components/index'); - const [coreStart, depsStart] = await core.getStartServices(); - const dslService = new DSLService(coreStart.http); - const savedObjects = new SavedObjects(coreStart.http); - const timestampUtils = new TimestampUtils(dslService, pplService); - return Observability( - coreStart, - depsStart, - params, - pplService, - dslService, - savedObjects, - timestampUtils, - qm - ); + const BASE_URL = core.http.basePath.prepend('/app/observability-dashboards#'); + setupDeps.dashboard.registerDashboardProvider({ + appId: 'observability-panel', + savedObjectsType: 'observability-panel', + savedObjectsName: 'Observability', + editUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}/edit`, + viewUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}`, + createLinkText: 'Observability Dashboard', + createSortText: 'Observability Dashboard', + createUrl: `${BASE_URL}/create`, + }); + + const OBSERVABILITY_APP_CATEGORIES: Record = Object.freeze({ + observability: { + id: 'observability', + label: i18n.translate('core.ui.observabilityNavList.label', { + defaultMessage: 'Observability', + }), + order: observabilityPluginOrder, }, }); + const appMountWithStartPage = (startPage: string) => async (params: AppMountParameters) => { + const { Observability } = await import('./components/index'); + const [coreStart, depsStart] = await core.getStartServices(); + const dslService = new DSLService(coreStart.http); + const savedObjects = new SavedObjects(coreStart.http); + const timestampUtils = new TimestampUtils(dslService, pplService); + + return Observability( + coreStart, + depsStart as AppPluginStartDependencies, + params, + pplService, + dslService, + savedObjects, + timestampUtils, + qm, + startPage + ); + }; + + core.application.register({ + id: observabilityApplicationsID, + title: observabilityApplicationsTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityApplicationsPluginOrder, + mount: appMountWithStartPage('applications'), + }); + + core.application.register({ + id: observabilityLogsID, + title: observabilityLogsTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityLogsPluginOrder, + mount: appMountWithStartPage('logs'), + }); + + core.application.register({ + id: observabilityMetricsID, + title: observabilityMetricsTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityMetricsPluginOrder, + mount: appMountWithStartPage('metrics'), + }); + + core.application.register({ + id: observabilityTracesID, + title: observabilityTracesTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityTracesPluginOrder, + mount: appMountWithStartPage('traces'), + }); + + core.application.register({ + id: observabilityNotebookID, + title: observabilityNotebookTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityNotebookPluginOrder, + mount: appMountWithStartPage('notebooks'), + }); + + core.application.register({ + id: observabilityPanelsID, + title: observabilityPanelsTitle, + category: OBSERVABILITY_APP_CATEGORIES.observability, + order: observabilityPanelsPluginOrder, + mount: appMountWithStartPage('dashboards'), + }); + const embeddableFactory = new ObservabilityEmbeddableFactoryDefinition(async () => ({ getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, @@ -106,16 +197,16 @@ export class ObservabilityPlugin title: OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, description: OBSERVABILITY_EMBEDDABLE_DESCRIPTION, icon: OBSERVABILITY_EMBEDDABLE_ICON, - aliasApp: observabilityID, - aliasPath: `#/event_analytics/explorer/?${CREATE_TAB_PARAM_KEY}=${CREATE_TAB_PARAM[TAB_CHART_ID]}`, + aliasApp: observabilityLogsID, + aliasPath: `#/explorer/?${CREATE_TAB_PARAM_KEY}=${CREATE_TAB_PARAM[TAB_CHART_ID]}`, stage: 'production', appExtensions: { visualizations: { docTypes: [VISUALIZATION_SAVED_OBJECT], toListItem: ({ id, attributes, updated_at: updatedAt }) => ({ description: attributes?.description, - editApp: observabilityID, - editUrl: `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${id}`, + editApp: observabilityLogsID, + editUrl: `#/explorer/${VISUALIZATION_SAVED_OBJECT}:${id}`, icon: OBSERVABILITY_EMBEDDABLE_ICON, id, savedObjectType: VISUALIZATION_SAVED_OBJECT, @@ -131,8 +222,17 @@ export class ObservabilityPlugin // Return methods that should be available to other plugins return {}; } + public start(core: CoreStart): ObservabilityStart { + const pplService: PPLService = new PPLService(core.http); + + coreRefs.http = core.http; + coreRefs.savedObjectsClient = core.savedObjects.client; + coreRefs.pplService = pplService; + coreRefs.toasts = core.notifications.toasts; + return {}; } + public stop() {} } diff --git a/public/services/data_fetchers/ppl/ppl_data_fetcher.ts b/public/services/data_fetchers/ppl/ppl_data_fetcher.ts index e83b45dc78..e621910c5d 100644 --- a/public/services/data_fetchers/ppl/ppl_data_fetcher.ts +++ b/public/services/data_fetchers/ppl/ppl_data_fetcher.ts @@ -32,7 +32,7 @@ export class PPLDataFetcher extends DataFetcherBase implements IDataFetcher { ) { super(); // index/index patterns for this search - this.queryIndex = this.getIndex(this.searchParams.query); + this.queryIndex = this.getIndex(this.searchParams.query.rawQuery); } async setTimestamp(index: string) { @@ -186,7 +186,7 @@ export class PPLDataFetcher extends DataFetcherBase implements IDataFetcher { tabId, query: { finalQuery, - [RAW_QUERY]: query, + [RAW_QUERY]: query.rawQuery, [SELECTED_TIMESTAMP]: curTimestamp, }, }) diff --git a/public/services/saved_objects/saved_object_client/saved_objects_actions.ts b/public/services/saved_objects/saved_object_client/saved_objects_actions.ts index 2b96c6a6c3..5d8ae29d39 100644 --- a/public/services/saved_objects/saved_object_client/saved_objects_actions.ts +++ b/public/services/saved_objects/saved_object_client/saved_objects_actions.ts @@ -103,7 +103,7 @@ export class SavedObjectsActions { const remainingObjectIds = [ ...new Set( - idMap.non_osd.concat( + idMap.non_osd?.concat( Object.entries(responses.deleteResponseList) .filter(([_, status]) => status !== 'OK') .map(([id, _]) => id) diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts index d6708ae4fe..7eb552586c 100644 --- a/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts @@ -3,6 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + addVizToPanels, + uuidRx, +} from '../../../../../public/components/custom_panels/redux/panel_slice'; import { SavedQuerySaver } from './saved_query_saver'; export class SaveAsCurrentVisualization extends SavedQuerySaver { @@ -46,9 +50,14 @@ export class SaveAsCurrentVisualization extends SavedQuerySaver { } addToPanel({ selectedPanels, saveTitle, notifications, visId }) { + const { dispatch } = this.dispatchers; + const soPanels = selectedPanels.filter((panel) => uuidRx.test(panel.panel.id)); + const opsPanels = selectedPanels.filter((panel) => !uuidRx.test(panel.panel.id)); + dispatch(addVizToPanels(soPanels, visId)); + this.panelClient .updateBulk({ - selectedCustomPanels: selectedPanels, + selectedCustomPanels: opsPanels, savedVisualizationId: visId, }) .then((res: any) => { diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts index 1c174438d0..fa7b1c7034 100644 --- a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts @@ -24,11 +24,10 @@ export class SaveAsNewQuery extends SavedQuerySaver { const { batch, dispatch, changeQuery, updateTabName } = this.dispatchers; const { tabId, history, notifications, showPermissionErrorToast } = this.saveContext; const { name } = this.saveParams; - console.log('this.saveParams: ', this.saveParams); this.saveClient .create({ ...this.saveParams }) .then((res: any) => { - history.replace(`/event_analytics/explorer/${res.objectId}`); + history.replace(`/explorer/${res.objectId}`); notifications.toasts.addSuccess({ title: 'Saved successfully.', text: `New query '${name}' has been successfully saved.`, @@ -50,7 +49,7 @@ export class SaveAsNewQuery extends SavedQuerySaver { }) ); }); - history.replace(`/event_analytics/explorer/${res.objectId}`); + history.replace(`/explorer/${res.objectId}`); return res; }) .catch((error: any) => { diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts index b0c57d4c7d..814ef97e4a 100644 --- a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts @@ -3,6 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { forEach } from 'lodash'; +import { + addVizToPanels, + fetchPanel, + uuidRx, +} from '../../../../../public/components/custom_panels/redux/panel_slice'; import { SAVED_OBJECT_ID, SAVED_OBJECT_TYPE, @@ -10,6 +16,7 @@ import { } from '../../../../../common/constants/explorer'; import { ISavedObjectsClient } from '../../saved_object_client/client_interface'; import { SavedQuerySaver } from './saved_query_saver'; +import { addVisualizationPanel } from '../../../../../public/components/custom_panels/helpers/add_visualization_helper'; export class SaveAsNewVisualization extends SavedQuerySaver { constructor( @@ -47,7 +54,7 @@ export class SaveAsNewVisualization extends SavedQuerySaver { if (appLogEvents) { addVisualizationToPanel(res.objectId, name); } else { - history.replace(`/event_analytics/explorer/${res.objectId}`); + history.replace(`/explorer/${res.objectId}`); } batch(() => { @@ -76,21 +83,26 @@ export class SaveAsNewVisualization extends SavedQuerySaver { } addToPanel({ selectedPanels, saveTitle, notifications, visId }) { + const { dispatch } = this.dispatchers; + const soPanels = selectedPanels.filter((panel) => uuidRx.test(panel.panel.id)); + const opsPanels = selectedPanels.filter((panel) => !uuidRx.test(panel.panel.id)); + + dispatch(addVizToPanels(soPanels, visId)); this.panelClient .updateBulk({ - selectedCustomPanels: selectedPanels, + selectedCustomPanels: opsPanels, savedVisualizationId: visId, }) .then((res: any) => { notifications.toasts.addSuccess({ title: 'Saved successfully.', - text: `Visualization '${saveTitle}' has been successfully saved to operation panels.`, + text: `Visualization '${saveTitle}' has been successfully saved to Observability Dashboards.`, }); }) .catch((error: any) => { notifications.toasts.addError(error, { title: 'Failed to save', - text: `Cannot add Visualization '${saveTitle}' to operation panels`, + text: `Cannot add Visualization '${saveTitle}' to Observability Dashboards`, }); }); } diff --git a/server/adaptors/custom_panels/custom_panel_adaptor.ts b/server/adaptors/custom_panels/custom_panel_adaptor.ts index 2b2ba55dd0..5703a85dd1 100644 --- a/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { PanelType, VisualizationType } from '../../../common/types/custom_panels'; import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; -import { createDemoPanel } from '../../common/helpers/custom_panels/sample_panels'; +import { createDemoPanel } from '../../../common/constants/custom_panels'; interface boxType { x1: number; @@ -142,6 +142,7 @@ export class CustomPanelsAdaptor { } }; + // Rename an existing panel renamePanel = async (client: ILegacyScopedClusterClient, panelId: string, panelName: string) => { const updatePanelBody = { diff --git a/server/common/helpers/custom_panels/sample_panels.ts b/server/common/helpers/custom_panels/sample_panels.ts deleted file mode 100644 index 3ef7a31c8e..0000000000 --- a/server/common/helpers/custom_panels/sample_panels.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { v4 as uuidv4 } from 'uuid'; - -export const createDemoPanel = (savedVisualizationIds: string[]) => { - return { - name: '[Logs] Web traffic Panel', - visualizations: [ - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[0], - x: 4, - y: 6, - w: 8, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[1], - x: 0, - y: 2, - w: 12, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[2], - x: 0, - y: 0, - w: 4, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[3], - x: 4, - y: 0, - w: 4, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[4], - x: 8, - y: 0, - w: 4, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[5], - x: 0, - y: 4, - w: 4, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[6], - x: 0, - y: 6, - w: 4, - h: 2, - }, - { - id: 'panel_viz_' + uuidv4(), - savedVisualizationId: savedVisualizationIds[7], - x: 4, - y: 4, - w: 8, - h: 2, - }, - ], - timeRange: { to: 'now/y', from: 'now/y' }, - queryFilter: { query: '', language: 'ppl' }, - }; -}; diff --git a/server/plugin.ts b/server/plugin.ts index bf86ea997b..f315f809b4 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -10,6 +10,7 @@ import { Logger, Plugin, PluginInitializerContext, + SavedObjectsType, } from '../../../src/core/server'; import { OpenSearchObservabilityPlugin } from './adaptors/opensearch_observability_plugin'; import { PPLPlugin } from './adaptors/ppl_plugin'; @@ -43,6 +44,42 @@ export class ObservabilityPlugin }; }); + const obsPanelType: SavedObjectsType = { + name: 'observability-panel', + hidden: false, + namespaceType: 'single', + mappings: { + dynamic: false, + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + }, + }, + management: { + importableAndExportable: true, + getInAppUrl() { + return { + path: `/app/management/observability/settings`, + uiCapabilitiesPath: 'advancedSettings.show', + }; + }, + getTitle(obj) { + return `Observability Settings [${obj.id}]`; + }, + }, + migrations: { + '3.0.0': (doc) => ({ ...doc, description: '' }), + '3.0.1': (doc) => ({ ...doc, description: 'Some Description Text' }), + '3.0.2': (doc) => ({ ...doc, dateCreated: parseInt(doc.dateCreated || '0', 10) }), + }, + }; + + core.savedObjects.registerType(obsPanelType); + // Register server side APIs setupRoutes({ router, client: openSearchObservabilityClient }); diff --git a/server/routes/custom_panels/panels_router.ts b/server/routes/custom_panels/panels_router.ts index 9c23972924..93db008ac9 100644 --- a/server/routes/custom_panels/panels_router.ts +++ b/server/routes/custom_panels/panels_router.ts @@ -132,6 +132,49 @@ export function PanelsRouter(router: IRouter) { } ); + + // update an existing panel + router.post( + { + path: `${API_PREFIX}/panels/update`, + validate: { + body: schema.object({ + panelId: schema.string(), + panel: schema.any(), + }), + }, + }, + async ( + context, + request, + response + ): Promise> => { + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); + + try { + const responseBody = await customPanelBackend.updatePanel( + opensearchNotebooksClient, + request.body.panelId, + request.body.panel + ); + return response.ok({ + body: { + message: 'Panel Updated', + }, + }); + } catch (error: any) { + console.error('Issue in updating panel', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + // rename an existing panel router.post( { diff --git a/server/saved_objects/observability_saved_object.ts b/server/saved_objects/observability_saved_object.ts index 5c47a0c223..d8f8ca32d0 100644 --- a/server/saved_objects/observability_saved_object.ts +++ b/server/saved_objects/observability_saved_object.ts @@ -4,7 +4,7 @@ */ import { SavedObjectsType } from '../../../../src/core/server'; -import { observabilityID } from '../../common/constants/shared'; +import { observabilityID, observabilityLogsID } from '../../common/constants/shared'; import { VISUALIZATION_SAVED_OBJECT } from '../../common/types/observability_saved_object_attributes'; export const visualizationSavedObject: SavedObjectsType = { @@ -19,8 +19,8 @@ export const visualizationSavedObject: SavedObjectsType = { return obj.attributes.title; }, getInAppUrl(obj) { - const editPath = `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${obj.id}`; - const editUrl = `/app/${observabilityID}${editPath}`; + const editPath = `#/explorer/${VISUALIZATION_SAVED_OBJECT}:${obj.id}`; + const editUrl = `/app/${observabilityLogsID}${editPath}`; return { path: editUrl, uiCapabilitiesPath: 'observability.show', diff --git a/test/panels_constants.tsx b/test/panels_constants.tsx index b79dde38c0..8e938bdcc4 100644 --- a/test/panels_constants.tsx +++ b/test/panels_constants.tsx @@ -4,7 +4,7 @@ */ export const panelBreadCrumbs = [ - { text: 'Operational panels', href: '#/operational_panels/' }, + { text: 'Observability Dashboards', href: '#/operational_panels/' }, { text: 'Observability', href: 'observability#/' }, ]; diff --git a/test/setup.jest.ts b/test/setup.jest.ts index 7f612f9085..3a6397fe42 100644 --- a/test/setup.jest.ts +++ b/test/setup.jest.ts @@ -6,6 +6,7 @@ // import '@testing-library/jest-dom/extend-expect'; import { configure } from '@testing-library/react'; import { setOSDHttp, setOSDSavedObjectsClient } from '../common/utils'; +import { coreRefs } from '../public/framework/core_refs'; import { coreStartMock } from './__mocks__/coreMocks'; configure({ testIdAttribute: 'data-test-subj' }); @@ -57,3 +58,6 @@ jest.setTimeout(30000); setOSDHttp(coreStartMock.http); setOSDSavedObjectsClient(coreStartMock.savedObjects.client); +coreRefs.http = coreStartMock.http; +coreRefs.savedObjectsClient = coreStartMock.savedObjects.client; +coreRefs.toasts = coreStartMock.notifications.toasts; diff --git a/yarn.lock b/yarn.lock index 952d6ff8e9..e9cce93cea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -481,6 +481,14 @@ any-observable@^0.3.0: resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + arch@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" @@ -510,6 +518,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-wait-until@1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/async-wait-until/-/async-wait-until-1.2.6.tgz#b6d8ada89913028af1928ee078925af75862b108" + integrity sha512-7I1zd0bnMEo7WfLfDoLZp+iPYKv/dl7kcW8wphazZn+BAElTGvtkDuQuonr480JzkS7f42VcGyP90mk3+3IfWA== + async@^3.2.0: version "3.2.3" resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" @@ -557,6 +570,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + blob-util@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" @@ -575,6 +593,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -668,6 +693,21 @@ check-more-types@^2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -833,6 +873,15 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== +cypress-watch-and-reload@^1.10.6: + version "1.10.6" + resolved "https://registry.yarnpkg.com/cypress-watch-and-reload/-/cypress-watch-and-reload-1.10.6.tgz#52423344fa52b94b818652f524df0cbcafc6a1ad" + integrity sha512-OI+3zZFSfMOjCH2xO9SUFfBurusbDOXctNtC6Q8VTokIURP+r0cwWZ5NVt6Ty3dtIMrWfiBsT+zsgAPvbmfTkA== + dependencies: + async-wait-until "1.2.6" + chokidar "3.5.3" + ws "8.13.0" + cypress@^6.0.0: version "6.9.1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.9.1.tgz#ce1106bfdc47f8d76381dba63f943447883f864c" @@ -1268,6 +1317,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -1316,6 +1372,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1356,7 +1417,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob-parent@^5.0.0, glob-parent@^6.0.1: +glob-parent@^5.0.0, glob-parent@^6.0.1, glob-parent@~5.1.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -1613,6 +1674,13 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.4: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -1659,7 +1727,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.0, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1679,6 +1747,11 @@ is-installed-globally@^0.3.2: global-dirs "^2.0.1" is-path-inside "^3.0.1" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-observable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" @@ -2066,6 +2139,11 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize.css@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" @@ -2215,7 +2293,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -2472,6 +2550,13 @@ readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + redux-persist@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" @@ -2895,6 +2980,13 @@ tmp@~0.2.1: dependencies: rimraf "^3.0.0" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -3169,6 +3261,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + x-is-string@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"