From 946d9f66540691ad580f4649772fca1a6f0f59a5 Mon Sep 17 00:00:00 2001 From: Peter Fitzgibbons Date: Tue, 18 Apr 2023 14:02:40 -0700 Subject: [PATCH] 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 --- .../{3_panels.spec.js => 3_panels.spec.ts} | 346 +++++-- .cypress/plugins/index.js | 4 + .cypress/support/index.js | 2 + .github/CODEOWNERS | 2 - CODEOWNERS | 2 + MAINTAINERS.md | 11 +- common/constants/custom_panels.ts | 7 +- common/constants/shared.ts | 2 +- common/types/custom_panels.ts | 37 +- common/types/explorer.ts | 4 +- cypress.json | 5 +- package.json | 20 +- 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 | 17 +- .../components/application_analytics/home.tsx | 106 +- public/components/common/side_nav.tsx | 145 --- .../custom_panels/custom_panel_table.tsx | 101 +- .../custom_panels/custom_panel_view.tsx | 62 +- .../custom_panels/custom_panel_view_so.tsx | 722 +++++++++++++ .../helpers/add_visualization_helper.ts | 98 ++ .../helpers/modal_containers.tsx | 4 +- .../helpers/panel_state_reducer.ts | 15 + .../custom_panels/helpers/utils.tsx | 33 +- public/components/custom_panels/home.tsx | 309 +++--- .../panel_modules/empty_panel.tsx | 2 +- .../panel_grid/panel_grid_so.tsx | 222 ++++ .../visualization_flyout.tsx | 13 +- .../visualization_flyout_so.tsx | 446 ++++++++ .../custom_panels/redux/panel_slice.ts | 286 ++++++ .../explorer/events_views/docViewer.tsx | 3 +- .../explorer/events_views/doc_flyout.tsx | 18 +- .../events_views/surrounding_flyout.tsx | 68 +- .../event_analytics/explorer/log_explorer.tsx | 7 +- .../explorer/save_panel/save_panel.tsx | 26 +- .../__snapshots__/config_panel.test.tsx.snap | 958 ++++++++++++++++++ .../components/event_analytics/home/home.tsx | 11 +- public/components/event_analytics/index.tsx | 39 +- public/components/index.tsx | 4 +- public/components/metrics/index.tsx | 167 +-- .../components/metrics/top_menu/top_menu.tsx | 6 +- .../__snapshots__/note_table.test.tsx.snap | 14 +- .../__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 | 1 - .../notebooks/components/notebook.tsx | 2 +- .../paragraph_components/para_input.tsx | 3 +- .../components/common/legacy_route_helpers.ts | 8 +- .../__snapshots__/dashboard.test.tsx.snap | 30 +- .../dashboard/dashboard_content.tsx | 8 +- .../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 ++-- .../trace_analytics/trace_side_nav.tsx | 74 ++ public/framework/core_refs.ts | 32 + public/framework/redux/reducers/index.ts | 2 + public/plugin.ts | 178 +++- .../ppl/save_as_current_vis.ts | 11 +- .../ppl/save_as_new_vis.ts | 18 +- .../custom_panels/custom_panel_adaptor.ts | 1 + server/plugin.ts | 37 + server/routes/custom_panels/panels_router.ts | 43 + yarn.lock | 104 +- 76 files changed, 4377 insertions(+), 1162 deletions(-) rename .cypress/integration/{3_panels.spec.js => 3_panels.spec.ts} (64%) delete mode 100644 .github/CODEOWNERS create mode 100644 CODEOWNERS delete mode 100644 public/components/common/side_nav.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 diff --git a/.cypress/integration/3_panels.spec.js b/.cypress/integration/3_panels.spec.ts similarity index 64% rename from .cypress/integration/3_panels.spec.js rename to .cypress/integration/3_panels.spec.ts index 900472fa27..43cc625c2b 100644 --- a/.cypress/integration/3_panels.spec.js +++ b/.cypress/integration/3_panels.spec.ts @@ -18,25 +18,6 @@ import { 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,42 +79,126 @@ 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('Displays error toast for invalid panel name', () => { + clickCreatePanelButton(); + confirmModal(); + expectToastWith('Invalid Operational Panel name'); + }); + + 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'); }); - 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(); + describe('with a Legacy Panel', () => { + beforeEach(() => { + createLegacyPanel(); + moveToPanelHome(); + }); + + 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'); + }); }); - 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 SavedObjects Panel', () => { + beforeEach(() => { + createSavedObjectPanel(); + moveToPanelHome(); + cy.get('.euiTableRow').should('have.length', 1); + }); + + 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); + }); + + 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'); }); - cy.get('button[data-test-subj="runModalButton"]').click(); }); it('Searches existing panel', () => { + createLegacyPanel(); cy.get('input[data-test-subj="operationalPanelSearchBar"]') .focus() .type('this panel should not exist', { @@ -154,23 +219,10 @@ describe('Testing panels table', () => { .should('exist'); }); - 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'); - - 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('Create a panel for testing', () => { + moveToPanelHome(); // keep a panel for testing - cy.get('button[data-test-subj="customPanels__createNewPanels"]').click(); + clickCreatePanelButton(); cy.get('input.euiFieldText').focus().type(TEST_PANEL, { delay: 50, }); @@ -222,7 +274,7 @@ describe('Testing a panel', () => { cy.get(`input.euiFieldText[value="${TEST_PANEL} (copy)"]`) .focus() - .clear({ force: true }) + .clear({force: true}) .focus() .type('Renamed Panel', { delay: 200, @@ -295,9 +347,9 @@ describe('Testing a panel', () => { cy.get('h5[data-test-subj="visualizationHeader"]') .contains(PPL_VISUALIZATIONS_NAMES[1]) - .trigger('mousedown', { which: 1 }) - .trigger('mousemove', { clientX: 1100, clientY: 0 }) - .trigger('mouseup', { force: true }); + .trigger('mousedown', {which: 1}) + .trigger('mousemove', {clientX: 1100, clientY: 0}) + .trigger('mouseup', {force: true}); cy.get('button[data-test-subj="savePanelButton"]').click(); cy.wait(delay * 3); @@ -312,9 +364,9 @@ describe('Testing a panel', () => { cy.get('.react-resizable-handle') .eq(1) - .trigger('mousedown', { which: 1 }) - .trigger('mousemove', { clientX: 2000, clientY: 800 }) - .trigger('mouseup', { force: true }); + .trigger('mousedown', {which: 1}) + .trigger('mousemove', {clientX: 2000, clientY: 800}) + .trigger('mouseup', {force: true}); cy.get('button[data-test-subj="savePanelButton"]').click(); cy.wait(delay * 3); @@ -429,7 +481,7 @@ describe('Testing a panel', () => { cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]').trigger('mouseover').click(); cy.wait(1000); cy.get('[data-test-subj="eventExplorer__querySaveName"]') - .clear({ force: true }) + .clear({force: true}) .type(NEW_VISUALIZATION_NAME, { delay: 200, }); @@ -470,8 +522,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, @@ -482,3 +534,163 @@ describe('Clean up all test data', () => { cy.get('.euiTextAlign').contains('No Operational Panels').should('exist'); }); }); + +const moveToEventsHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/`); + cy.wait(delay * 3); +}; + +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 = () => { + 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: TEST_PANEL, + description: '', + dateCreated: 1681127334085, + dateModified: 1681127334085, + timeRange: { + to: 'now', + from: 'now-1d', + }, + queryFilter: { + query: '', + language: 'ppl', + }, + visualizations: [], + applicationId: '', + }, + }, + }) + .then((response) => console.log(response)); +}; + +const createLegacyPanel = () => { + 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: TEST_PANEL }, + }); +}; + +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/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/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 1dc11731b8..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams -* @opensearch-project/observability \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..1f9a96bf70 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams +* @pjfitzgibbons @anirudha @ps48 @kavithacm @derek-ho @joshuali925 @dai-chen @YANG-DB @rupal-bq @mengweieric @vamsi-amazon @swiddis \ No newline at end of file diff --git a/MAINTAINERS.md b/MAINTAINERS.md index c1c270bc94..522b84b1b1 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -12,4 +12,13 @@ | Rupal Mahajan | [rupal-bq](https://github.com/rupal-bq) | Amazon | | Derek Ho | [derek-ho](https://github.com/derek-ho) | Amazon | | Lior Perry | [YANG-DB](https://github.com/YANG-DB) | Amazon | -| Peter Fitzgibbons | [pjfitzgibbons](https://github.com/pjfitzgibbons) | Amazon | \ No newline at end of file +| Peter Fitzgibbons | [pjfitzgibbons](https://github.com/pjfitzgibbons) | Amazon | +| Simeon Widdis | [swiddis] (https://github.com/swiddis) | Amazon | + +## Emeritus Maintainers + +| Maintainer | GitHub ID | Affiliation | +| ----------------- | ------------------------------------------------------- | ----------- | +| Charlotte Henkle | [CEHENKLE](https://github.com/CEHENKLE) | Amazon | +| Anirudha Jadhav | [anirudha](https://github.com/anirudha) | Amazon | +| Nick Knize | [nknize](https://github.com/nknize) | Amazon | diff --git a/common/constants/custom_panels.ts b/common/constants/custom_panels.ts index 0c02b97a29..1062d63a29 100644 --- a/common/constants/custom_panels.ts +++ b/common/constants/custom_panels.ts @@ -4,5 +4,10 @@ */ 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 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_SAVED_OBJECT_TYPE = 'observability-panel'; + +export const CUSTOM_PANEL_SLICE = 'customPanel'; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index f1e936e123..a001232212 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -23,7 +23,7 @@ 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; 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..f775172a9e 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,20 @@ "license": "Apache-2.0", "scripts": { "osd": "node ../../scripts/osd", - "build": "yarn plugin_helpers build", + "build": "yarn plugin-helpers build", "test": "../../node_modules/.bin/jest --config ./test/jest.config.js", "cypress:run": "TZ=America/Los_Angeles cypress run", "cypress:open": "TZ=America/Los_Angeles cypress open", - "plugin_helpers": "node ../../scripts/plugin_helpers" + "plugin-helpers": "node ../../scripts/plugin_helpers", + "prepare": "husky install", + "lint:es": "node ../../scripts/eslint", + "lint": "yarn lint:es" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx}": [ + "yarn lint --fix", + "git add" + ] }, "dependencies": { "@algolia/autocomplete-core": "^1.4.1", @@ -21,13 +30,13 @@ "ag-grid-react": "^27.3.0", "antlr4": "4.8.0", "antlr4ts": "^0.5.0-alpha.4", - "performance-now": "^2.1.0", "plotly.js-dist": "^2.2.0", "postinstall": "^0.7.4", "react-graph-vis": "^1.0.5", "react-paginate": "^8.1.3", "react-plotly.js": "^2.5.1", - "redux-persist": "^6.0.0" + "redux-persist": "^6.0.0", + "performance-now": "^2.1.0" }, "devDependencies": { "@cypress/skip-test": "^2.6.1", @@ -36,8 +45,11 @@ "@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", + "husky": "6.0.0", "jest-dom": "^4.0.0", + "lint-staged": "^13.1.0", "ts-jest": "^29.1.0" }, "resolutions": { 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 ecfc017542..f17d46e1df 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,7 +19,10 @@ 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'; @@ -64,7 +67,7 @@ export function ServiceDetailFlyout(props: ServiceFlyoutProps) { ), getListItem( 'Connected services', - fields.connected_services + fields.connected_services && fields.connected_services.length ? fields.connected_services.reduce((prev: string, curr: string) => { return [prev, ', ', curr]; }) @@ -118,7 +121,15 @@ export function ServiceDetailFlyout(props: ServiceFlyoutProps) { }, [serviceName, fields, serviceMap, DSL, serviceMapIdSelected]); useEffect(() => { - const serviceDSL = filtersToDsl(mode, filters, query, processTimeStamp(startTime, mode), processTimeStamp(endTime, mode), 'app', appConfigs); + const serviceDSL = filtersToDsl( + mode, + filters, + query, + processTimeStamp(startTime, mode), + processTimeStamp(endTime, mode), + 'app', + appConfigs + ); handleServiceViewRequest(serviceName, http, serviceDSL, setFields, mode); handleServiceMapRequest(http, serviceDSL, mode, setServiceMap, serviceName); const spanDSL = filtersToDsl(mode, filters, query, startTime, endTime, 'app', appConfigs); diff --git a/public/components/application_analytics/home.tsx b/public/components/application_analytics/home.tsx index 89dec51cfa..a97f56f34d 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); } @@ -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/custom_panels/custom_panel_table.tsx b/public/components/custom_panels/custom_panel_table.tsx index 50054523ad..c0914e0b74 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/public/components/custom_panels/custom_panel_table.tsx @@ -32,6 +32,9 @@ import { import React, { ReactElement, useEffect, useState } from 'react'; import moment from 'moment'; import _ from 'lodash'; +import { useHistory, useLocation } from 'react-router-dom'; +import { coreRefs } from 'public/framework/core_refs'; +import { useDispatch, useSelector } from 'react-redux'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE, @@ -43,7 +46,13 @@ import { CustomPanelListType } 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, + fetchPanels, + newPanelTemplate, + renameCustomPanel, + selectPanelList, +} from './redux/panel_slice'; /* * "CustomPanelTable" module, used to view all the saved panels @@ -62,12 +71,8 @@ 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; @@ -75,16 +80,13 @@ interface Props { 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); @@ -93,15 +95,21 @@ export const CustomPanelTable = ({ const location = useLocation(); const history = useHistory(); + const dispatch = useDispatch(); + useEffect(() => { setBreadcrumbs(parentBreadcrumbs); - fetchCustomPanels(); + dispatch(fetchPanels()); }, []); + // useEffect(() => + // console.log({ customPanels, selectedCustomPanels }, [customPanels, selectedCustomPanels]) + // ); + 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 +122,43 @@ export const CustomPanelTable = ({ }; const onCreate = async (newCustomPanelName: string) => { - createCustomPanel(newCustomPanelName); + const newPanel = newPanelTemplate(newCustomPanelName); + dispatch(createPanel(newPanel)); closeModal(); }; const onRename = async (newCustomPanelName: string) => { - renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id); + dispatch(renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id)); closeModal(); }; const onClone = async (newName: string) => { - cloneCustomPanel(newName, selectedCustomPanels[0].id); + const sourcePanel = selectedCustomPanels[0]; + const { id, ...newPanel } = { ...sourcePanel, title: sourcePanel.title + ' (copy)' }; + + dispatch(createPanel(newPanel)); closeModal(); }; const onDelete = async () => { - const toastMessage = `Custom Panels ${ - selectedCustomPanels.length > 1 ? 's' : ' ' + selectedCustomPanels[0].name + const toastMessage = `Observability Dashboards ${ + selectedCustomPanels.length > 1 ? 's' : ' ' + selectedCustomPanels[0].title } successfully deleted!`; const PanelList = selectedCustomPanels.map((panel) => panel.id); deleteCustomPanelList(PanelList, toastMessage); closeModal(); }; - const createPanel = () => { + const createPanelModal = () => { setModalLayout( getCustomModal( onCreate, () => { - closeModal() + closeModal(); history.goBack(); }, 'Name', - 'Create operational panel', + 'Create Observability Dashboard', 'Cancel', 'Create', undefined, @@ -162,10 +174,10 @@ export const CustomPanelTable = ({ onRename, closeModal, 'Name', - 'Rename Panel', + 'Rename Dashboard', 'Cancel', 'Rename', - selectedCustomPanels[0].name, + selectedCustomPanels[0].title, CREATE_PANEL_MESSAGE ) ); @@ -178,10 +190,10 @@ export const CustomPanelTable = ({ onClone, closeModal, 'Name', - 'Duplicate Panel', + 'Duplicate Dashboard', 'Cancel', 'Duplicate', - selectedCustomPanels[0].name + ' (copy)', + selectedCustomPanels[0].title + ' (copy)', CREATE_PANEL_MESSAGE ) ); @@ -189,7 +201,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[] => [ >; + // console.log('rendering', { customPanels, selectedCustomPanels }); return (
@@ -301,7 +316,7 @@ export const CustomPanelTable = ({ -

Operational panels

+

Observability dashboards

@@ -310,14 +325,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 +347,12 @@ export const CustomPanelTable = ({ isOpen={isActionsPopoverOpen} closePopover={() => setIsActionsPopoverOpen(false)} > - + - - Create panel + + Create Dashboard @@ -353,7 +364,7 @@ export const CustomPanelTable = ({ setSearchQuery(e.target.value)} /> @@ -363,7 +374,7 @@ export const CustomPanelTable = ({ items={ searchQuery ? customPanels.filter((customPanel) => - customPanel.name.toLowerCase().includes(searchQuery.toLowerCase()) + customPanel.title.toLowerCase().includes(searchQuery.toLowerCase()) ) : customPanels } @@ -391,10 +402,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 +416,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..99075358bf 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,17 @@ 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 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, @@ -64,10 +66,10 @@ 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'; /* - * "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 @@ -142,6 +144,8 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onEditClick, onAddClick, } = props; + + const [panel, setPanel] = useState(); const [openPanelName, setOpenPanelName] = useState(''); const [panelCreatedTime, setPanelCreatedTime] = useState(''); const [pplFilterValue, setPPLFilterValue] = useState(''); @@ -183,6 +187,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 +196,7 @@ 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); }); }; @@ -208,14 +213,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 +235,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 +254,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onRename, closeModal, 'Name', - 'Rename Panel', + 'Rename Dashboard', 'Cancel', 'Rename', openPanelName, @@ -264,9 +265,18 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }; const onClone = async (newCustomPanelName: string) => { - cloneCustomPanel(newCustomPanelName, panelId).then((id: string) => { - window.location.assign(`${last(parentBreadcrumbs)!.href}${id}`); - }); + const newPanel = { + ...panel, + title: newCustomPanelName, + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + } as PanelType; + const newSOPanel = await coreRefs.savedObjectsClient!.create( + CUSTOM_PANELS_SAVED_OBJECT_TYPE, + newPanel + ); + + window.location.assign(`${last(parentBreadcrumbs)!.href}${newSOPanel.id}`); closeModal(); }; @@ -276,7 +286,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onClone, closeModal, 'Name', - 'Duplicate Panel', + 'Duplicate Dashboard', 'Cancel', 'Duplicate', openPanelName + ' (copy)', @@ -382,7 +392,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 +410,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 +463,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { onClick={() => setPanelsMenuPopover(true)} disabled={addVizDisabled} > - Panel actions + Dashboard Actions ); @@ -488,7 +498,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 +506,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, }, { - name: 'Rename panel', + name: 'Rename Dashboard', 'data-test-subj': 'renamePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -504,7 +514,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, }, { - name: 'Duplicate panel', + name: 'Duplicate Dashboard', 'data-test-subj': 'duplicatePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -512,7 +522,7 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }, }, { - name: 'Delete panel', + name: 'Delete Dashboard', 'data-test-subj': 'deletePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); @@ -523,7 +533,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..19eb6cde4d --- /dev/null +++ b/public/components/custom_panels/custom_panel_view_so.tsx @@ -0,0 +1,722 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +/* // eslint-disable no-console */ +/* eslint-disable react-hooks/exhaustive-deps */ + +import { useCallback } from 'react'; +import { + EuiBreadcrumb, + EuiButton, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, + EuiSuperDatePicker, + EuiTitle, + OnTimeChangeProps, + ShortDate, +} from '@elastic/eui'; +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, useSelector } from 'react-redux'; +import { useRef } from 'react'; +import DSLService from '../../services/requests/dsl'; +import { CoreStart, SimpleSavedObject } 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 { CustomPanelType, PanelType } from '../../../common/types/custom_panels'; +import { PanelGridSO } from './panel_modules/panel_grid/panel_grid_so'; + +import { getCustomModal } from './helpers/modal_containers'; +import PPLService from '../../services/requests/ppl'; +import { + isDateValid, + convertDateTime, + isPPLFilterValid, + isNameValid, + prependRecentlyUsedRange, +} from './helpers/utils'; +import { UI_DATE_FORMAT } from '../../../common/constants/shared'; +import { VisaulizationFlyout } from './panel_modules/visualization_flyout'; +import { uiSettingsService } from '../../../common/utils'; +import { PPLReferenceFlyout } from '../common/helpers'; +import { Autocomplete } from '../common/search/autocomplete'; +import { + parseGetSuggestions, + onItemSelect, + parseForIndices, +} from '../common/search/autocomplete_logic'; +import { AddVisualizationPopover } from './helpers/add_visualization_popover'; +import { DeleteModal } from '../common/helpers/delete_modal'; +import { VisaulizationFlyoutSO } from './panel_modules/visualization_flyout/visualization_flyout_so'; +import { addVisualizationPanel } from './helpers/add_visualization_helper'; +import { + clonePanel, + createPanel, + fetchPanel, + newPanelTemplate, + selectPanel, + setPanel, + setPanelEt, + setPanelId, + setPanelSt, + updatePanel, +} from './redux/panel_slice'; +import { coreRefs } from '../../framework/core_refs'; + +/* + * "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 + * setToast: create Toast function + * 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[]; + 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; + childBreadcrumbs?: EuiBreadcrumb[]; + appId?: string; + updateAvailabilityVizId?: any; + onAddClick?: any; +} + +export const CustomPanelViewSO = (props: CustomPanelViewProps) => { + const { + panelId, + page, + appId, + pplService, + dslService, + chrome, + parentBreadcrumbs, + childBreadcrumbs, + updateAvailabilityVizId, + deleteCustomPanel, + cloneCustomPanel, + setToast, + onEditClick, + onAddClick, + } = props; + + const dispatch = useDispatch(); + + 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 () => { + deleteCustomPanel(panelId, panel?.title).then((res) => { + setTimeout(() => { + window.location.assign(`${last(parentBreadcrumbs)!.href}`); + }, 1000); + }); + closeModal(); + }; + + const deletePanel = () => { + setModalLayout( + + ); + showModal(); + }; + + const renamePanel = () => { + setModalLayout( + getCustomModal( + onRename, + closeModal, + 'Name', + 'Rename Dashboard', + 'Cancel', + 'Rename', + panel.title, + CREATE_PANEL_MESSAGE + ) + ); + showModal(); + }; + + const onClone = async (newCustomPanelName: string) => { + 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 startEdit = () => { + setIsEditing(true); + }; + + const applyEdits = useCallback(() => { + dispatch(updatePanel(panel)); + setIsEditing(false); + setEditActionType('save'); + }, [panel]); + + const cancelEdit = () => { + console.log('cancelEdits'); + dispatch(fetchPanel(panelId)); + setIsEditing(false); + }; + + 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.log(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) => { + // http + // .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { + // body: JSON.stringify({ + // panelId, + // savedVisualizationId, + // }), + // }) + // .then(async (res) => { + // setPanelVisualizations(res.visualizations); + // setToast(`Visualization ${visualzationTitle} successfully added!`, 'success'); + // }) + // .catch((err) => { + // setToast(`Error in adding ${visualzationTitle} visualization to the panel`, 'danger'); + // console.error(err); + // }); + }; + + const cancelButton = ( + + Cancel + + ); + + const saveButton = ( + + Save + + ); + + const editButton = ( + + Edit + + ); + + const addButton = ( + + Add + + ); + + // Panel Actions Button + const panelActionsButton = ( + setPanelsMenuPopover(true)} + disabled={addVizDisabled} + > + Dashboard Actions + + ); + + const addVisualizationToCurrentPanel = async ({ + savedVisualizationId, + oldVisualizationId, + }: { + savedVisualizationId: string; + oldVisualizationId?: string; + }) => { + const allVisualizations = panel!.visualizations; + + const visualizationsWithNewPanel = addVisualizationPanel( + savedVisualizationId, + oldVisualizationId, + allVisualizations + ); + + const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; + try { + dispatch(updatePanel(updatedPanel)); + } catch (err) { + setToast('Error adding visualization to this Dashboard', 'danger'); + console.error(err?.body?.message || err); + } + }; + + 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(() => { + !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/add_visualization_helper.ts b/public/components/custom_panels/helpers/add_visualization_helper.ts new file mode 100644 index 0000000000..5c57ab53a2 --- /dev/null +++ b/public/components/custom_panels/helpers/add_visualization_helper.ts @@ -0,0 +1,98 @@ +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); + } +}; 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..3605602ea2 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -370,25 +370,19 @@ 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)); +}; /** * Convert an ObservabilitySavedVisualization into SavedVisualizationType, * which is used in panels. @@ -521,3 +515,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..6c86318e26 100644 --- a/public/components/custom_panels/home.tsx +++ b/public/components/custom_panels/home.tsx @@ -2,32 +2,35 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ -import { EuiBreadcrumb, EuiGlobalToastList, EuiLink, ShortDate } from '@elastic/eui'; +import { EuiBreadcrumb, EuiGlobalToastList, 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 { useDispatch } 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 { 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, - CUSTOM_PANELS_DOCUMENTATION_URL, + CUSTOM_PANELS_SAVED_OBJECT_TYPE, } 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 { coreRefs } from '../../framework/core_refs'; +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 { deletePanel, fetchPanels, uuidRx } from './redux/panel_slice'; + +// import { ObjectFetcher } from '../common/objectFetcher'; /* * "Home" module is initial page for Operantional Panels @@ -37,16 +40,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,14 +62,24 @@ 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 dispatch = useDispatch(); + + const customPanelBreadCrumbs = [ + ...parentBreadcrumbs, + { + text: 'Dashboards', + href: `${observabilityPanelsID}#/`, + }, + ]; const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { if (!text) text = ''; @@ -72,143 +88,34 @@ export const Home = ({ }; const onEditClick = (savedVisualizationId: string) => { - window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); - }; - - // 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); - }; - - // Creates a new CustomPanel - const createCustomPanel = (newCustomPanelName: string) => { - if (!isNameValid(newCustomPanelName)) { - setToast('Invalid Operational Panel name', 'danger'); - window.location.assign(`${_.last(parentBreadcrumbs)!.href}`); - return; - } - - 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); - }); + window.location.assign(`${observabilityLogsID}#/explorer/${savedVisualizationId}`); }; - // 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) => { - 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); - }); + const deletePanelSO = (customPanelIdList: string[]) => { + const soPanelIds = customPanelIdList.filter((id) => id.match(uuidRx)); + return Promise.all( + soPanelIds.map((id) => + coreRefs.savedObjectsClient?.delete(CUSTOM_PANELS_SAVED_OBJECT_TYPE, id) + ) + ); }; - // 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); - }); + const deletePanels = (customPanelIdList: string[]) => { + const panelIds = customPanelIdList.filter((id) => !id.match(uuidRx)); + const concatList = panelIds.toString(); + return http.delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList); }; // Deletes multiple existing Operational Panels const deleteCustomPanelList = (customPanelIdList: string[], toastMessage: string) => { - const concatList = customPanelIdList.toString(); - return http - .delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList) + Promise.all([deletePanelSO(customPanelIdList), deletePanels(customPanelIdList)]) .then((res) => { - setcustomPanelData((prevCustomPanelData) => { - return prevCustomPanelData.filter( - (customPanel) => !customPanelIdList.includes(customPanel.id) - ); - }); - setToast(toastMessage); - return res; + // setcustomPanelData((prevCustomPanelData) => { + // return prevCustomPanelData.filter( + // (customPanel) => !customPanelIdList.includes(customPanel.id) + // ); + // }); + // setToast(toastMessage); }) .catch((err) => { setToast( @@ -219,26 +126,30 @@ export const Home = ({ }); }; - // Deletes an existing Operational Panel - const deleteCustomPanel = (customPanelId: string, customPanelName: string) => { + // Deletes an existing Observability Dashboard + const deleteCustomPanel = async (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!`); + dispatch(fetchPanels()); + setToast(`Observability Dashboard "${customPanelName}" successfully deleted!`); return res; }) .catch((err) => { setToast( - 'Error deleting Operational Panel, please make sure you have the correct permission.', + 'Error deleting Observability Dashboard, please make sure you have the correct permission.', 'danger' ); console.error(err.body.message); }); }; + // Deletes an existing SO Observability Dashboard + const deleteCustomPanelSO = async (customPanelId: string, customPanelName: string) => { + dispatch(deletePanel(customPanelId)); + // TODO: toast here + }; + const addSamplePanels = async () => { try { setLoading(true); @@ -278,19 +189,21 @@ export const Home = ({ }), }) .then((res) => { - setcustomPanelData([...customPanelData, ...res.demoPanelsData]); + dispatch(fetchPanels()); }); 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 ( -
+ { @@ -300,54 +213,60 @@ export const Home = ({ 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..1f9a68e443 --- /dev/null +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx @@ -0,0 +1,222 @@ +/* + * 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 = useCallback( + 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 || []); + }, + [panel] + ); + + // 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..99fb39112f 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); @@ -182,11 +182,15 @@ export const VisaulizationFlyout = ({ }), }) .then(async (res) => { + console.log('addVisualization Replacement', res); setPanelVisualizations(res.visualizations); 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 { @@ -198,6 +202,7 @@ export const VisaulizationFlyout = ({ }), }) .then(async (res) => { + console.log('addVisualization New', res); setPanelVisualizations(res.visualizations); setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); }) 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..5ffb427592 --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx @@ -0,0 +1,446 @@ +/* + * 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 _, { isError } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; +import { FlyoutContainers } from '../../../common/flyout_containers'; +import { displayVisualization, getQueryResponse, isDateValid } from '../../helpers/utils'; +import { convertDateTime } from '../../helpers/utils'; +import PPLService from '../../../../services/requests/ppl'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; +import { + BoxType, + PplResponse, + SavedVisualizationType, + VisualizationType, + VizContainerError, +} from '../../../../../common/types/custom_panels'; +import './visualization_flyout.scss'; +import { uiSettingsService } from '../../../../../common/utils'; +import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server'; +import { replaceVizInPanel, selectPanel } from '../../redux/panel_slice'; + +/* + * 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 + * setToast: function to set toast in the panel + * 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']; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; + savedObjects: CoreStart['savedObjects']; + pplService: PPLService; + setPanelVisualizations: React.Dispatch>; + isFlyoutReplacement?: boolean | undefined; + replaceVisualizationId?: string | undefined; + appId?: string; + addVisualizationPanel: any; +} + +export const VisaulizationFlyoutSO = ({ + panelId, + appId = '', + pplFilterValue, + closeFlyout, + start, + end, + http, + setToast, + savedObjects, + pplService, + setPanelVisualizations, + isFlyoutReplacement, + replaceVisualizationId, + addVisualizationPanel, +}: VisualizationFlyoutSOProps) => { + const dispatch = useDispatch(); + + 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, 'left')) { + return false; + } + + if (selectValue === '') { + setToast('Please make a valid selection', 'danger', undefined, 'left'); + return false; + } + + return true; + }; + + const addVisualization = () => { + if (!isInputValid()) return; + + if (isFlyoutReplacement) { + // http + // .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations/replace`, { + // body: JSON.stringify({ + // panelId, + // savedVisualizationId: selectValue, + // oldVisualizationId: replaceVisualizationId, + // }), + // }) + // .then(async (res) => { + // setPanelVisualizations(res.visualizations); + // setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + // }) + // .catch((err) => { + // setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + // console.error(err); + // }); + dispatch(replaceVizInPanel(panel, replaceVisualizationId, selectValue)); + } else { + const visualizationsWithNewPanel = addVisualizationPanel({ + savedVisualizationId: selectValue, + }); + + // http + // .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { + // body: JSON.stringify({ + // panelId, + // savedVisualizationId: selectValue, + // }), + // }) + // .then(async (res) => { + // setPanelVisualizations(res.visualizations); + // setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + // }) + // .catch((err) => { + // setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + // console.error(err); + // }); + } + closeFlyout(); + }; + + const onRefreshPreview = () => { + if (!isInputValid()) return; + + getQueryResponse( + pplService, + pplQuery, + newVisualizationType, + start, + end, + setPreviewData, + setPreviewLoading, + setIsPreviewError, + pplFilterValue, + newVisualizationTimeField + ); + }; + + const timeRange = ( + + + 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 http + .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations`) + .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 operational panels', 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..a3028a4dfd --- /dev/null +++ b/public/components/custom_panels/redux/panel_slice.ts @@ -0,0 +1,286 @@ +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, +} 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 { addVisualizationPanel } from '../helpers/add_visualization_helper'; + +interface InitialState { + id: string; + panel: CustomPanelType; + panelList: CustomPanelType[]; +} + +export const newPanelTemplate = (newName) => ({ + title: newName, + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + visualizations: [], + queryFilter: { language: '', query: '' }, + timeRange: { from: 'now', to: 'now-1d' }, +}); + +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): PanelType => ({ + ...newPanelTemplate(''), + ...panel, +}); + +export const selectPanelList = (rootState): CustomPanelType[] => rootState.customPanel.panelList; + +// export const selectPanelList = createSelector( +// rootState => { console.log("selectPanelList", { rootState }); return rootState.customPanel.panelList }, +// panelList => panelList.map(p => p as CustomPanelListType) +// ); + +/* + ** 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}$/; + +const isUuid = (id) => !!id.match(uuidRx); + +export const updatePanel = (panel: CustomPanelType) => async (dispatch, getState) => { + try { + if (isUuid(panel.id)) await updateSavedObjectPanel(panel); + else await updateLegacyPanel(panel); + + dispatch(setPanel(panel)); + const panelList = getState().customPanel.panelList.map((p) => (p.id === panel.id ? panel : p)); + dispatch(setPanelList(panelList)); + } catch (err) { + console.log('Error updating Dashboard', { err, panel }); + } +}; + +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 }; + try { + dispatch(updatePanel(updatedPanel)); + } catch (err) { + console.error(err?.body?.message || err); + } + }); +}; + +export const replaceVizInPanel = (oldPanel, oldVizId, vizId) => 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 }; + try { + dispatch(updatePanel(updatedPanel)); + } catch (err) { + console.error(err?.body?.message || err); + } +}; + +export const deletePanel = (id) => async (dispatch, getState) => { + await savedObjectPanelsClient.delete(id); + const panelList: CustomPanelType[] = getState().panelList.filter((p) => p.id !== id); + dispatch(setPanelList(panelList)); +}; + +export const createPanel = (panel) => async (dispatch, getState) => { + const newSOPanel = await savedObjectPanelsClient.create(panel); + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); + + window.location.replace(`#/${newPanel.id}`); +}; + +export const clonePanel = (panel, newPanelName) => async (dispatch, getState) => { + 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)); + + window.location.replace(`#/${newPanel.id}`); +}; + +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 +) => { + if (!isNameValid(editedCustomPanelName)) { + console.log('Invalid Observability Dashboard name', 'danger'); + return Promise.reject(); + } + + const panel = getState().customPanel.panelList.find((p) => p.id === id); + const updatedPanel = { ...panel, title: editedCustomPanelName }; + dispatch(updatePanel(updatedPanel)); + + // try { + // // await savePanelFn(editedCustomPanelId, editedCustomPanelName); + + // // setcustomPanelData((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) { + // console.log( + // 'Error renaming Observability Dashboard, please make sure you have the correct permission.', + // 'danger' + // ); + // console.error(err.body.message); + // } +}; + +/* + ** 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/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/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/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index a712df4bb4..67c7e58376 100644 --- a/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -604,6 +604,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -920,6 +927,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -1219,6 +1233,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -2609,6 +2630,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -2940,6 +2968,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -3276,6 +3311,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -3547,6 +3589,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -3884,6 +3933,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -4191,6 +4247,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -4666,6 +4729,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], } } @@ -5269,6 +5339,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -5595,6 +5672,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], } } @@ -6198,6 +6282,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -6565,6 +6656,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], } } @@ -7168,6 +7266,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -7873,6 +7978,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -8845,6 +8957,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -10316,6 +10435,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -11808,6 +11934,13 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, ], }, + Object { + "editor": [Function], + "id": "color-theme", + "mapTo": "colorTheme", + "name": "Color theme", + "schemas": Array [], + }, ], }, Object { @@ -14241,6 +14374,831 @@ exports[`Config panel component Renders config panel with visualization data 1`]
+ +
+
+ + +
+
+ +
+
+ +
+
+ + + + + + +
+ +
+
+ +
+
+ + +
+
+
diff --git a/public/components/event_analytics/home/home.tsx b/public/components/event_analytics/home/home.tsx index f69489dfc5..f82141a954 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" > @@ -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/top_menu/top_menu.tsx b/public/components/metrics/top_menu/top_menu.tsx index cd3d17e3b3..8ab98fb2e3 100644 --- a/public/components/metrics/top_menu/top_menu.tsx +++ b/public/components/metrics/top_menu/top_menu.tsx @@ -21,8 +21,8 @@ import { } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { useDispatch, useSelector } from 'react-redux'; -import { uiSettingsService } from '../../../../common/utils'; import React, { useEffect, useState } from 'react'; +import { uiSettingsService } from '../../../../common/utils'; import { MetricType } from '../../../../common/types/metrics'; import { resolutionOptions } from '../../../../common/constants/metrics'; import './top_menu.scss'; @@ -92,7 +92,7 @@ export const TopMenu = ({ const [originalPanelVisualizations, setOriginalPanelVisualizations] = useState([]); const [isSavePanelOpen, setIsSavePanelOpen] = useState(false); const [selectedPanelOptions, setSelectedPanelOptions] = useState< - EuiComboBoxOptionOption[] | undefined + Array> | undefined >([]); // toggle between panel edit mode @@ -206,7 +206,7 @@ export const TopMenu = ({ console.error(message, e); setToast('Issue in saving metrics', 'danger'); } - setToast('Saved metrics to panels successfully!'); + setToast('Saved metrics to Dashboards successfully!'); } }; 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..552a09624b 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`] = ` > { }, ] 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..229f3d61c8 100644 --- a/public/components/notebooks/components/main.tsx +++ b/public/components/notebooks/components/main.tsx @@ -17,7 +17,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'; diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index 2d63e61ca8..072e7ab3c6 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -306,7 +306,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); 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/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..fe56704a04 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, @@ -93,7 +93,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; @@ -154,7 +154,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.'); // } // }, @@ -170,7 +170,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.'); // } // }, 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/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/framework/core_refs.ts b/public/framework/core_refs.ts new file mode 100644 index 0000000000..e9a3e0e604 --- /dev/null +++ b/public/framework/core_refs.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { HttpStart } 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; + 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/plugin.ts b/public/plugin.ts index e9b2815d65..7726e80de1 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -3,12 +3,37 @@ * 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, - observabilityPluginOrder, - observabilityTitle, + observabilityApplicationsID, + observabilityApplicationsPluginOrder, + observabilityApplicationsTitle, + observabilityTracesTitle, + observabilityMetricsID, + observabilityMetricsPluginOrder, + observabilityMetricsTitle, + observabilityNotebookID, + observabilityNotebookPluginOrder, + observabilityNotebookTitle, + observabilityTracesID, + observabilityTracesPluginOrder, + observabilityPanelsID, + observabilityPanelsTitle, + observabilityPanelsPluginOrder, + observabilityLogsID, + observabilityLogsTitle, + observabilityLogsPluginOrder, } from '../common/constants/shared'; import { QueryManager } from '../common/query_manager'; import { VISUALIZATION_SAVED_OBJECT } from '../common/types/observability_saved_object_attributes'; @@ -20,6 +45,16 @@ 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 { uiSettingsService } from '../common/utils'; +// import { QueryManager } from '../common/query_manager'; +import { DashboardSetup } from '../../../src/plugins/dashboard/public'; +import { SavedObject } from '../../../src/core/public'; +import { coreRefs } from './framework/core_refs'; + +// export class ObservabilityPlugin implements Plugin { +// constructor(private initializerContext: PluginInitializerContext) {} + +// public setup(core: CoreSetup, { dashboard }: { dashboard: DashboardSetup }): {} { import { OBSERVABILITY_EMBEDDABLE, OBSERVABILITY_EMBEDDABLE_DESCRIPTION, @@ -33,6 +68,7 @@ import DSLService from './services/requests/dsl'; import PPLService from './services/requests/ppl'; import SavedObjects from './services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from './services/timestamp/timestamp'; +import { observabilityID } from '../common/constants/shared'; import { AppPluginStartDependencies, ObservabilitySetup, @@ -47,6 +83,7 @@ export class ObservabilityPlugin core: CoreSetup, setupDeps: SetupDependencies ): ObservabilitySetup { + console.log('core: ', core, ', setupDeps: ', setupDeps); uiSettingsService.init(core.uiSettings, core.notifications); const pplService = new PPLService(core.http); const qm = new QueryManager(); @@ -66,34 +103,101 @@ 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 - ); + // // redirect legacy notebooks URL to current URL under observability + // if (window.location.pathname.includes('application_analytics')) { + // window.location.assign(convertLegacyAppAnalyticsUrl(window.location)); + // } + + setupDeps.dashboard.registerDashboardProvider({ + appId: 'observability-panel', + savedObjectsType: 'observability-panel', + savedObjectsName: 'Observability', + editUrlPathFn: (obj: SavedObject) => `/app/observability-dashboards#/${obj.id}/edit`, + viewUrlPathFn: (obj: SavedObject) => `/app/observability-dashboards#/${obj.id}`, + createLinkText: 'Observability Dashboard', + createSortText: 'Observability Dashboard', + createUrl: '/app/observability-dashboards#/create', + }); + + const OBSERVABILITY_APP_CATEGORIES: Record = Object.freeze({ + observability: { + id: 'observability', + label: i18n.translate('core.ui.observabilityNavList.label', { + defaultMessage: 'Observability', + }), + order: 3000, }, }); + const appMountWithStartPage = (startPage: string) => async (params: AppMountParameters) => { + console.log('start page: ', startPage); + 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 +210,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 +235,16 @@ 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; + return {}; } + public stop() {} } 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_vis.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts index b0c57d4c7d..5ca6bb8c40 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( @@ -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..f687dca9c4 100644 --- a/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -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/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/yarn.lock b/yarn.lock index 952d6ff8e9..c57b4d842f 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,16 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +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 +575,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 +598,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@^3.0.2, 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: + fast-json-stable-stringify "2.x" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -668,6 +698,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 +878,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" @@ -1316,6 +1370,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 +1415,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 +1672,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 +1725,12 @@ 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-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +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== @@ -2066,6 +2137,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 +2291,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, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -2472,6 +2548,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" @@ -3169,6 +3252,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" @@ -3189,6 +3277,16 @@ yargs-parser@^21.0.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yaml@^2.1.3: + version "2.2.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4" + integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw== + +yargs-parser@^21.0.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"