From ebda89c36a94a36912722e31cae5f7544a9f4cda Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Mon, 27 Dec 2021 16:14:56 -0500 Subject: [PATCH] Add observability visualization to notebooks (#351) * added observability viz support to notes Signed-off-by: Shenoy Pratik * updated tests, clone para, zeppelin parser Signed-off-by: Shenoy Pratik * updated observability viz links & cypress Signed-off-by: Shenoy Pratik * updated tests Signed-off-by: Shenoy Pratik * removed inputType, merged both viz options Signed-off-by: Shenoy Pratik * resolved merge conflict Signed-off-by: Shenoy Pratik * updated links, adaptors, tests Signed-off-by: Shenoy Pratik * removed unused files, updated workflow Signed-off-by: Shenoy Pratik * updated UI dateformat for observability viz Signed-off-by: Shenoy Pratik * updated jest timeout Signed-off-by: Shenoy Pratik * updated notebook tests Signed-off-by: Shenoy Pratik --- .cypress/CYPRESS_TESTS.md | 3 +- .cypress/integration/notebooks.spec.js | 92 +++++++++++----- .cypress/integration/panels.spec.js | 2 +- ...-observability-test-and-build-workflow.yml | 5 +- common/constants/custom_panels.ts | 2 +- common/constants/explorer.ts | 2 +- common/constants/notebooks.ts | 2 +- common/constants/shared.ts | 3 +- common/constants/trace_analytics.ts | 2 +- public/components/app.tsx | 1 + .../custom_panel_table.test.tsx.snap | 8 +- .../visualization_container.tsx | 16 ++- .../__snapshots__/note_table.test.tsx.snap | 4 +- .../__snapshots__/notebook.test.tsx.snap | 8 +- .../components/__tests__/notebook.test.tsx | 22 +++- .../components/helpers/default_parser.tsx | 6 +- .../components/helpers/zeppelin_parser.tsx | 24 ++++- .../components/notebooks/components/main.tsx | 3 + .../notebooks/components/notebook.tsx | 25 ++++- .../__snapshots__/paragraphs.test.tsx.snap | 4 +- .../__tests__/para_input.test.tsx | 32 ++++-- .../paragraph_components/para_input.tsx | 24 ++++- .../paragraph_components/para_output.tsx | 50 +++++++-- .../paragraph_components/paragraphs.tsx | 100 ++++++++++++------ .../__snapshots__/search_bar.test.tsx.snap | 10 ++ .../service_map_scale.test.tsx.snap | 4 +- .../__snapshots__/dashboard.test.tsx.snap | 34 ++++-- .../__snapshots__/service_view.test.tsx.snap | 2 +- .../__snapshots__/services.test.tsx.snap | 34 ++++-- .../service_breakdown_panel.test.tsx.snap | 1 + .../__snapshots__/traces.test.tsx.snap | 34 ++++-- server/adaptors/notebooks/default_backend.ts | 31 ++++-- server/adaptors/notebooks/notebook_adaptor.ts | 4 +- server/adaptors/notebooks/zeppelin_backend.ts | 46 ++++---- server/routes/notebooks/paraRouter.ts | 1 + test/jest.config.js | 4 +- test/setup.jest.ts | 2 + 37 files changed, 473 insertions(+), 174 deletions(-) diff --git a/.cypress/CYPRESS_TESTS.md b/.cypress/CYPRESS_TESTS.md index 3b5597adf..bba2db259 100644 --- a/.cypress/CYPRESS_TESTS.md +++ b/.cypress/CYPRESS_TESTS.md @@ -102,7 +102,8 @@ The observability plugin currently has 4 modules in it. Each of the modules have * Renders input only mode * Renders output only mode * Duplicates paragraphs -* Adds a visualization paragraph +* Adds a dashboards visualization paragraph +* Adds an observability visualization paragraph * Adds a SQL query paragraph * Adds a PPL query paragraph * Clears outputs diff --git a/.cypress/integration/notebooks.spec.js b/.cypress/integration/notebooks.spec.js index d06fe81ff..a44f531f5 100644 --- a/.cypress/integration/notebooks.spec.js +++ b/.cypress/integration/notebooks.spec.js @@ -11,17 +11,35 @@ import { MARKDOWN_TEXT, SAMPLE_URL, SQL_QUERY_TEXT, - PPL_QUERY_TEXT -} from "../utils/constants"; + PPL_QUERY_TEXT, +} from '../utils/constants'; -import { skipOn } from '@cypress/skip-test' +import { SAMPLE_PANEL } from '../utils/panel_constants'; + +import { skipOn } from '@cypress/skip-test'; describe('Adding sample data and visualization', () => { it('Adds sample flights data for visualization paragraph', () => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/home#/tutorial_directory/sampleData`); - cy.get('div[data-test-subj="sampleDataSetCardflights"]').contains(/(Add|View) data/).click(); + cy.get('div[data-test-subj="sampleDataSetCardflights"]') + .contains(/(Add|View) data/) + .click(); }); -}) + + it('Add sample observability data', () => { + cy.visit( + `${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/operational_panels/` + ); + cy.get('.euiButton__text').contains('Actions').click(); + cy.get('.euiContextMenuItem__text').contains('Add samples').click(); + cy.get('.euiModalHeader__title[data-test-subj="confirmModalTitleText"]') + .contains('Add samples') + .should('exist'); + cy.get('.euiButton__text').contains('Yes').click(); + cy.wait(delay * 5); + cy.get('.euiTableCellContent').contains(SAMPLE_PANEL).should('exist'); + }); +}); describe('Testing notebooks table', () => { beforeEach(() => { @@ -31,7 +49,9 @@ describe('Testing notebooks table', () => { it('Displays error toast for invalid notebook name', () => { cy.get('.euiButton__text').contains('Create notebook').click(); cy.wait(delay); - cy.get('.euiButton__text').contains(/^Create$/).click(); + cy.get('.euiButton__text') + .contains(/^Create$/) + .click(); cy.wait(delay); cy.get('.euiToastHeader__title').contains('Invalid notebook name').should('exist'); @@ -41,7 +61,9 @@ describe('Testing notebooks table', () => { cy.get('.euiButton__text').contains('Create notebook').click(); cy.wait(delay); cy.get('input.euiFieldText').type(TEST_NOTEBOOK); - cy.get('.euiButton__text').contains(/^Create$/).click(); + cy.get('.euiButton__text') + .contains(/^Create$/) + .click(); cy.wait(delay); cy.contains(TEST_NOTEBOOK).should('exist'); @@ -81,7 +103,9 @@ describe('Testing notebooks table', () => { cy.get('input.euiFieldSearch').type(TEST_NOTEBOOK + ' (copy) (rename)'); cy.wait(delay); - cy.get('a.euiLink').contains(TEST_NOTEBOOK + ' (copy) (rename)').should('exist'); + cy.get('a.euiLink') + .contains(TEST_NOTEBOOK + ' (copy) (rename)') + .should('exist'); }); it('Deletes notebooks', () => { @@ -104,7 +128,9 @@ describe('Testing notebooks table', () => { cy.get('.euiButton__text').contains('Create notebook').click(); cy.wait(delay); cy.get('input.euiFieldText').type(TEST_NOTEBOOK); - cy.get('.euiButton__text').contains(/^Create$/).click(); + cy.get('.euiButton__text') + .contains(/^Create$/) + .click(); cy.wait(delay * 2); }); }); @@ -114,7 +140,7 @@ describe('Test reporting integration if plugin installed', () => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); cy.get('.euiTableCellContent').contains(TEST_NOTEBOOK).click(); cy.wait(delay * 3); - cy.get('body').then($body => { + cy.get('body').then(($body) => { skipOn($body.find('#reportingActionsButton').length <= 0); }); }); @@ -138,24 +164,18 @@ describe('Test reporting integration if plugin installed', () => { cy.wait(delay); cy.get('button.euiContextMenuItem:nth-child(3)').contains('Create report definition').click(); cy.wait(delay); - cy.location('pathname', { timeout: 60000 }).should( - 'include', - '/reports-dashboards' - ); + cy.location('pathname', { timeout: 60000 }).should('include', '/reports-dashboards'); cy.wait(delay); cy.get('#reportSettingsName').type('Create notebook on-demand report'); cy.get('#createNewReportDefinition').click({ force: true }); }); - it ('View reports homepage from context menu', () => { + it('View reports homepage from context menu', () => { cy.get('#reportingActionsButton').click(); cy.wait(delay); cy.get('button.euiContextMenuItem:nth-child(4)').contains('View reports').click(); cy.wait(delay); - cy.location('pathname', { timeout: 60000 }).should( - 'include', - '/reports-dashboards' - ); + cy.location('pathname', { timeout: 60000 }).should('include', '/reports-dashboards'); }); }); @@ -235,7 +255,7 @@ describe('Testing paragraphs', () => { cy.get(`a[href="${SAMPLE_URL}"]`).should('have.length.gte', 2); }); - it('Adds a visualization paragraph', () => { + it('Adds a dashboards visualization paragraph', () => { cy.contains('Add paragraph').click(); cy.wait(delay); cy.get('.euiContextMenuItem__text').contains('Visualization').click(); @@ -247,13 +267,12 @@ describe('Testing paragraphs', () => { cy.get('.euiButton__text').contains('Browse').click(); cy.wait(delay); - cy.get('.euiFieldSearch').focus().type('{uparrow}{uparrow}{enter}') + cy.get('.euiFieldSearch').focus().type('[Flights] Flight Count and Average Ticket Price{enter}'); cy.wait(delay); cy.get('.euiButton__text').contains('Select').click(); cy.wait(delay); cy.get('.euiButton__text').contains('Run').click(); cy.wait(delay); - cy.get('div.visualization').should('exist'); }); @@ -273,6 +292,27 @@ describe('Testing paragraphs', () => { cy.get('.euiDataGrid__overflow').should('exist'); }); + it('Adds an observability visualization paragraph', () => { + cy.contains('Add paragraph').click(); + cy.wait(delay); + cy.get('.euiContextMenuItem__text').contains('Visualization').click(); + cy.wait(delay); + + cy.get('.euiButton__text').contains('Run').click(); + cy.wait(delay); + cy.get('.euiTextColor').contains('Visualization is required.').should('exist'); + + cy.get('.euiButton__text').contains('Browse').click(); + cy.wait(delay); + cy.get('.euiFieldSearch').focus().type('[Logs] Count total requests by tags{enter}'); + cy.wait(delay); + cy.get('.euiButton__text').contains('Select').click(); + cy.wait(delay); + cy.get('.euiButton__text').contains('Run').click(); + cy.wait(delay); + cy.get('h5').contains('[Logs] Count total requests by tags').should('exist'); + }); + it('Adds a PPL query paragraph', () => { cy.contains('Add paragraph').click(); cy.wait(delay); @@ -326,7 +366,7 @@ describe('Testing paragraphs', () => { cy.get('.euiContextMenuItem__text').contains('Code block').click(); cy.wait(delay); - cy.get('.euiText').contains('[4] OpenSearch Dashboards visualization').should('exist'); + cy.get('.euiText').contains('[4] Visualization').should('exist'); cy.get('.euiText').contains('[5] Code block').should('exist'); }); @@ -337,7 +377,7 @@ describe('Testing paragraphs', () => { cy.get('.euiContextMenuItem__text').contains('Move to bottom').click(); cy.wait(delay); - cy.get('.euiText').contains('[3] OpenSearch Dashboards visualization').should('exist'); + cy.get('.euiText').contains('[3] Visualization').should('exist'); }); it('Duplicates and renames the notebook', () => { @@ -359,7 +399,9 @@ describe('Testing paragraphs', () => { cy.reload(); cy.wait(delay * 3); - cy.get('.euiTitle').contains(TEST_NOTEBOOK + ' (copy) (rename)').should('exist'); + cy.get('.euiTitle') + .contains(TEST_NOTEBOOK + ' (copy) (rename)') + .should('exist'); cy.get(`a[href="${SAMPLE_URL}"]`).should('have.length.gte', 2); }); diff --git a/.cypress/integration/panels.spec.js b/.cypress/integration/panels.spec.js index 5f9ebd72e..3def6c45a 100644 --- a/.cypress/integration/panels.spec.js +++ b/.cypress/integration/panels.spec.js @@ -414,7 +414,7 @@ describe('Add samples and clean up all test data', () => { it('Delete visualizations from event analytics', () => { moveToEventsHome(); - cy.get('.euiButtonEmpty__text').contains('Rows per page: 10').click(); + cy.get('span.euiButtonEmpty__text').contains('Rows per page: 10').click(); cy.get('.euiContextMenuItem__text').contains('50 rows').click(); cy.get('.euiCheckbox__input[data-test-subj="checkboxSelectAll"]').click(); cy.wait(delay); diff --git a/.github/workflows/dashboards-observability-test-and-build-workflow.yml b/.github/workflows/dashboards-observability-test-and-build-workflow.yml index 42203497c..07f51afad 100644 --- a/.github/workflows/dashboards-observability-test-and-build-workflow.yml +++ b/.github/workflows/dashboards-observability-test-and-build-workflow.yml @@ -51,11 +51,10 @@ jobs: cd OpenSearch-Dashboards/plugins/dashboards-observability yarn osd bootstrap - # TODO enable unit tests when ready - - name: Test Panels + - name: Test all dashboards-observability modules run: | cd OpenSearch-Dashboards/plugins/dashboards-observability - yarn test ./public/components/custom_panels/ --coverage + yarn test --coverage - name: Upload coverage uses: codecov/codecov-action@v1 diff --git a/common/constants/custom_panels.ts b/common/constants/custom_panels.ts index 02466ef2e..fc8978fd1 100644 --- a/common/constants/custom_panels.ts +++ b/common/constants/custom_panels.ts @@ -4,5 +4,5 @@ */ export const CUSTOM_PANELS_API_PREFIX = '/api/observability/operational_panels'; -export const CUSTOM_PANELS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugins/operational-panels/'; +export const CUSTOM_PANELS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability/operational-panels/'; export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this custom panel.'; diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index 0d69bd696..6c2949c1b 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugins/event-analytics/' +export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability/event-analytics/' export const RAW_QUERY = 'rawQuery'; export const FINAL_QUERY = 'finalQuery'; export const SELECTED_DATE_RANGE = 'selectedDateRange'; diff --git a/common/constants/notebooks.ts b/common/constants/notebooks.ts index 29775bf3e..cdea7cc5d 100644 --- a/common/constants/notebooks.ts +++ b/common/constants/notebooks.ts @@ -7,7 +7,7 @@ export const NOTEBOOKS_API_PREFIX = '/api/observability/notebooks'; export const NOTEBOOKS_SELECTED_BACKEND = 'DEFAULT'; // ZEPPELIN || DEFAULT export const NOTEBOOKS_FETCH_SIZE = 1000; export const CREATE_NOTE_MESSAGE = 'Enter a name to describe the purpose of this notebook.'; -export const NOTEBOOKS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugins/notebooks/'; +export const NOTEBOOKS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability/notebooks/'; export const zeppelinURL = 'http://localhost:8080'; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 4838a54ab..23fdd835c 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -26,7 +26,8 @@ export const observabilityTitle = 'Observability'; export const observabilityPluginOrder = 6000; // Shared Constants -export const PPL_DOCUMENTATION_URL ='https://opensearch.org/docs/latest/observability-plugins/ppl/commands/' +export const SQL_DOCUMENTATION_URL ='https://opensearch.org/docs/latest/search-plugins/sql/index/' +export const PPL_DOCUMENTATION_URL ='https://opensearch.org/docs/latest/observability/ppl/commands/' export const UI_DATE_FORMAT = 'MM/DD/YYYY hh:mm A'; export const PPL_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export const PPL_STATS_REGEX = /\|\s*stats/i; diff --git a/common/constants/trace_analytics.ts b/common/constants/trace_analytics.ts index 8c663154b..c81154616 100644 --- a/common/constants/trace_analytics.ts +++ b/common/constants/trace_analytics.ts @@ -11,7 +11,7 @@ export const SERVICE_MAP_MAX_NODES = 500; // size limit when requesting edge related queries, not necessarily the number of edges export const SERVICE_MAP_MAX_EDGES = 1000; export const TRACES_MAX_NUM = 3000; -export const TRACE_ANALYTICS_DOCUMENTATION_LINK = 'https://opensearch.org/docs/latest/observability-plugins/trace/index/'; +export const TRACE_ANALYTICS_DOCUMENTATION_LINK = 'https://opensearch.org/docs/latest/observability/trace/index/'; export const TRACE_ANALYTICS_INDICES_ROUTE = '/api/observability/trace_analytics/indices'; export const TRACE_ANALYTICS_DSL_ROUTE = '/api/observability/trace_analytics/query'; diff --git a/public/components/app.tsx b/public/components/app.tsx index 4c7e57642..e6d6aafcb 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -59,6 +59,7 @@ export const App = ({ DepsStart.dashboard.DashboardContainerByValueRenderer } http={http} + pplService={pplService} setBreadcrumbs={chrome.setBreadcrumbs} parentBreadcrumb={parentBreadcrumb} notifications={notifications} diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap index f56dbeca6..78b0e283c 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap @@ -160,12 +160,12 @@ exports[`Panels Table Component renders empty panel table container 1`] = ` @@ -763,12 +763,12 @@ exports[`Panels Table Component renders panel table container 1`] = ` diff --git a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx index 62c6292a0..ca067d3f7 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx +++ b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx @@ -51,10 +51,11 @@ type Props = { fromTime: string; toTime: string; onRefresh: boolean; - cloneVisualization: (visualzationTitle: string, savedVisualizationId: string) => void; pplFilterValue: string; - showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; - removeVisualization: (visualizationId: string) => void; + usedInNotebooks?: boolean; + cloneVisualization?: (visualzationTitle: string, savedVisualizationId: string) => void; + showFlyout?: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; + removeVisualization?: (visualizationId: string) => void; }; export const VisualizationContainer = ({ @@ -66,8 +67,9 @@ export const VisualizationContainer = ({ fromTime, toTime, onRefresh, - cloneVisualization, pplFilterValue, + usedInNotebooks, + cloneVisualization, showFlyout, removeVisualization, }: Props) => { @@ -81,7 +83,7 @@ export const VisualizationContainer = ({ const onActionsMenuClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); const closeActionsMenu = () => setIsPopoverOpen(false); - const popoverPanel = [ + let popoverPanel = [ , ]; + if (usedInNotebooks){ + popoverPanel = [popoverPanel[0]] + } + const loadVisaulization = async () => { await renderSavedVisualization( http, 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 26e894d79..fc64007a0 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 @@ -58,7 +58,7 @@ exports[` spec renders the component 1`] = ` @@ -971,7 +971,7 @@ exports[` spec renders the empty component 1`] = ` diff --git a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap b/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap index 2d198c238..d184baf15 100644 --- a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap +++ b/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap @@ -267,14 +267,14 @@ exports[` spec renders the component 1`] = ` class="euiTitle euiTitle--small euiCard__title" id="random_html_idTitle" > - OpenSearch Dashboards visualization + Visualization

- Import OpenSearch Dashboards visualizations to the notes. + Import OpenSearch Dashboards or Observability visualizations to the notes.

@@ -564,14 +564,14 @@ exports[` spec renders the empty component 1`] = ` class="euiTitle euiTitle--small euiCard__title" id="random_html_idTitle" > - OpenSearch Dashboards visualization + Visualization

- Import OpenSearch Dashboards visualizations to the notes. + Import OpenSearch Dashboards or Observability visualizations to the notes.

diff --git a/public/components/notebooks/components/__tests__/notebook.test.tsx b/public/components/notebooks/components/__tests__/notebook.test.tsx index 9f8ea4469..eaabb3654 100644 --- a/public/components/notebooks/components/__tests__/notebook.test.tsx +++ b/public/components/notebooks/components/__tests__/notebook.test.tsx @@ -6,6 +6,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { configure, mount, shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +import PPLService from '../../../../services/requests/ppl'; import React from 'react'; import { HttpResponse } from '../../../../../../../src/core/public'; import httpClientMock from '../../../../../test/__mocks__/httpClientMock'; @@ -34,7 +35,8 @@ global.fetch = jest.fn(() => describe(' spec', () => { configure({ adapter: new Adapter() }); - it('renders the empty component', () => { + it('renders the empty component', async () => { + const pplService = new PPLService(httpClientMock); const setBreadcrumbs = jest.fn(); const renameNotebook = jest.fn(); const cloneNotebook = jest.fn(); @@ -45,6 +47,7 @@ describe(' spec', () => { history.replace = jest.fn(); const utils = render( spec', () => { /> ); expect(utils.container.firstChild).toMatchSnapshot(); - - utils.getByText("Add code block").click(); - utils.getByText("Add visualization").click(); + utils.getByText('Add code block').click(); + utils.getByText('Add visualization').click(); }); it('renders the component', async () => { + const pplService = new PPLService(httpClientMock); const setBreadcrumbs = jest.fn(); const renameNotebook = jest.fn(); const cloneNotebook = jest.fn(); @@ -77,6 +80,16 @@ describe(' spec', () => { Promise.resolve(({ ...sampleNotebook1, path: sampleNotebook1.name, + visualizations: [ + { + id: 'oiuccXwBYVazWqOO1e06', + name: 'Flight Count by Origin', + query: + 'source=opensearch_dashboards_sample_data_flights | fields Carrier,FlightDelayMin | stats sum(FlightDelayMin) as delays by Carrier', + type: 'bar', + timeField: 'timestamp', + }, + ], savedVisualizations: Array.from({ length: 5 }, (v, k) => ({ label: `vis-${k}`, key: `vis-${k}`, @@ -85,6 +98,7 @@ describe(' spec', () => { ); const utils = render( { // Param: Default Backend Paragraph const parseVisualization = (paraObject: any) => { try { - if (paraObject.input.inputType === 'VISUALIZATION') { + if (paraObject.input.inputType.includes('VISUALIZATION')) { let vizContent = paraObject.input.inputText; const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); @@ -55,7 +55,7 @@ const parseVisualization = (paraObject: any) => { if (vizContent !== '') { const { panels, timeRange } = JSON.parse(vizContent); visStartTime = timeRange.from; - visEndTime = timeRange.to + visEndTime = timeRange.to; visSavedObjId = panels['1'].explicitInput.savedObjectId; } return { diff --git a/public/components/notebooks/components/helpers/zeppelin_parser.tsx b/public/components/notebooks/components/helpers/zeppelin_parser.tsx index bb4ecbae2..f2293c3a7 100644 --- a/public/components/notebooks/components/helpers/zeppelin_parser.tsx +++ b/public/components/notebooks/components/helpers/zeppelin_parser.tsx @@ -11,6 +11,7 @@ import { ParaType } from '../../../common'; const visualizationPrefix = '%sh #vizobject:'; +const observabilityVisualizationPrefix = '%sh #observabilityviz:'; const langSupport = { '%sh': 'shell', @@ -65,7 +66,10 @@ const parseText = (paraObject: any) => { // Param: Zeppelin Paragraph const parseVisualization = (paraObject: any) => { let vizContent = ''; - if ('text' in paraObject && paraObject.text.substring(0, 15) === visualizationPrefix) { + if ( + paraObject.hasOwnProperty('text') && + paraObject.text.substring(0, 15) === visualizationPrefix + ) { if (paraObject.title !== 'VISUALIZATION') { throw new Error('Visualization parse issue'); } @@ -74,12 +78,26 @@ const parseVisualization = (paraObject: any) => { isViz: true, VizObject: vizContent, }; - } else { + } + + if ( + paraObject.hasOwnProperty('text') && + paraObject.text.substring(0, 22) === observabilityVisualizationPrefix + ) { + if (paraObject.title !== 'OBSERVABILITY_VISUALIZATION') { + throw new Error('Visualization parse issue'); + } + vizContent = paraObject.text.substring(22); return { - isViz: false, + isViz: true, VizObject: vizContent, }; } + + return { + isViz: false, + VizObject: vizContent, + }; }; // This parser is used to get paragraph id diff --git a/public/components/notebooks/components/main.tsx b/public/components/notebooks/components/main.tsx index 08d8bed8e..971bcc666 100644 --- a/public/components/notebooks/components/main.tsx +++ b/public/components/notebooks/components/main.tsx @@ -5,6 +5,7 @@ import { EuiGlobalToastList, EuiLink } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import PPLService from '../../../services/requests/ppl'; import React, { ReactChild } from 'react'; import { Route, Switch } from 'react-router'; import { HashRouter, RouteComponentProps } from 'react-router-dom'; @@ -32,6 +33,7 @@ import { NoteTable } from './note_table'; type MainProps = RouteComponentProps & { DashboardContainerByValueRenderer: DashboardStart['DashboardContainerByValueRenderer']; http: CoreStart['http']; + pplService: PPLService; notifications: CoreStart['notifications']; parentBreadcrumb: ChromeBreadcrumb; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; @@ -307,6 +309,7 @@ export class Main extends React.Component { path="/notebooks/:id" render={(props) => ( { // Function to clone a paragraph cloneParaButton = (para: ParaType, index: number) => { let inputType = 'CODE'; - if (para.isVizualisation === true) { + if (para.typeOut[0] === 'VISUALIZATION') { inputType = 'VISUALIZATION'; } + if (para.typeOut[0] === 'OBSERVABILITY_VISUALIZATION') { + inputType = 'OBSERVABILITY_VISUALIZATION'; + } if (index !== -1) { return this.addPara(index, para.inp, inputType); } @@ -469,7 +474,12 @@ export class Notebook extends Component { }; // Backend call to update and run contents of paragraph - updateRunParagraph = (para: ParaType, index: number, vizObjectInput?: string) => { + updateRunParagraph = ( + para: ParaType, + index: number, + vizObjectInput?: string, + paraType?: string + ) => { this.showParagraphRunning(index); if (vizObjectInput) { para.inp = this.state.vizPrefix + vizObjectInput; // "%sh check" @@ -479,6 +489,7 @@ export class Notebook extends Component { noteId: this.props.openedNoteId, paragraphId: para.uniqueId, paragraphInput: para.inp, + paragraphType: paraType || '', }; return this.props.http @@ -661,7 +672,10 @@ export class Notebook extends Component { } configureViewParameter(id: string) { - this.props.history.replace({ ...this.props.location, search: `view=${id}` }); + this.props.history.replace({ + ...this.props.location, + search: `view=${id}`, + }); } componentDidMount() { @@ -989,6 +1003,7 @@ export class Notebook extends Component { > this.setPara(para, index)} dateModified={this.state.paragraphs[index]?.dateModified} @@ -1070,8 +1085,8 @@ export class Notebook extends Component { } - title="OpenSearch Dashboards visualization" - description="Import OpenSearch Dashboards visualizations to the notes." + title="Visualization" + description="Import OpenSearch Dashboards or Observability visualizations to the notes." footer={ this.addPara(0, '', 'VISUALIZATION')} diff --git a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/paragraphs.test.tsx.snap b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/paragraphs.test.tsx.snap index b44729828..19842841f 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/paragraphs.test.tsx.snap +++ b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/paragraphs.test.tsx.snap @@ -134,7 +134,7 @@ exports[` spec renders the component 1`] = ` Specify the input language on the first line using %[language type]. Supported languages include markdown,
@@ -155,7 +155,7 @@ exports[` spec renders the component 1`] = ` and diff --git a/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx b/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx index e12a899e6..de07ab37d 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx +++ b/public/components/notebooks/components/paragraph_components/__tests__/para_input.test.tsx @@ -12,6 +12,18 @@ import { ParaInput } from '../para_input'; describe(' spec', () => { configure({ adapter: new Adapter() }); + const visOptions1 = Array.from({ length: 5 }, (v, k) => ({ + label: `visualization-${k}`, + key: `key-${k}`, + })); + const visOptions2 = Array.from({ length: 5 }, (v, k) => ({ + label: `visualization-${k}`, + key: `key-${k}`, + })); + const visOptions = [ + { label: 'VisOptions1', options: visOptions1 }, + { label: 'VisOptions2', options: visOptions2 }, + ]; it('renders the markdown component', () => { const para = sampleParsedParagraghs1[0]; @@ -21,6 +33,7 @@ describe(' spec', () => { const setEndTime = jest.fn(); const setIsOutputStale = jest.fn(); const setSelectedVisOption = jest.fn(); + const setVisType = jest.fn(); const utils = render( spec', () => { visOptions={[]} selectedVisOption={[]} setSelectedVisOption={setSelectedVisOption} + setVisType={setVisType} /> ); expect(utils.container.firstChild).toMatchSnapshot(); @@ -49,10 +63,7 @@ describe(' spec', () => { const setEndTime = jest.fn(); const setIsOutputStale = jest.fn(); const setSelectedVisOption = jest.fn(); - const visOptions = Array.from({ length: 5 }, (v, k) => ({ - label: `visualization-${k}`, - key: `key-${k}`, - })); + const setVisType = jest.fn(); const utils = render( spec', () => { setEndTime={setEndTime} setIsOutputStale={setIsOutputStale} visOptions={visOptions} - selectedVisOption={[visOptions[0]]} + selectedVisOption={[visOptions1[0]]} setSelectedVisOption={setSelectedVisOption} + setVisType={setVisType} /> ); expect(utils.container.firstChild).toMatchSnapshot(); @@ -82,6 +94,7 @@ describe(' spec', () => { const setEndTime = jest.fn(); const setIsOutputStale = jest.fn(); const setSelectedVisOption = jest.fn(); + const setVisType = jest.fn(); const utils = render( spec', () => { visOptions={[]} selectedVisOption={[]} setSelectedVisOption={setSelectedVisOption} + setVisType={setVisType} /> ); const textarea = utils.container.querySelectorAll('textarea#editorArea')[0]; @@ -113,10 +127,7 @@ describe(' spec', () => { const setEndTime = jest.fn(); const setIsOutputStale = jest.fn(); const setSelectedVisOption = jest.fn(); - const visOptions = Array.from({ length: 5 }, (v, k) => ({ - label: `visualization-${k}`, - key: `key-${k}`, - })); + const setVisType = jest.fn(); const utils = render( spec', () => { setEndTime={setEndTime} setIsOutputStale={setIsOutputStale} visOptions={visOptions} - selectedVisOption={[visOptions[0]]} + selectedVisOption={[visOptions2[0]]} setSelectedVisOption={setSelectedVisOption} + setVisType={setVisType} /> ); const datepicker = utils.container.querySelectorAll( diff --git a/public/components/notebooks/components/paragraph_components/para_input.tsx b/public/components/notebooks/components/paragraph_components/para_input.tsx index 19728be87..5cc41ddd0 100644 --- a/public/components/notebooks/components/paragraph_components/para_input.tsx +++ b/public/components/notebooks/components/paragraph_components/para_input.tsx @@ -9,6 +9,7 @@ import { EuiCodeBlock, EuiComboBox, EuiComboBoxOptionOption, + EuiSelectableOption, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -58,6 +59,7 @@ export const ParaInput = (props: { visOptions: EuiComboBoxOptionOption[]; selectedVisOption: EuiComboBoxOptionOption[]; setSelectedVisOption: (newOption: EuiComboBoxOptionOption[]) => void; + setVisType: React.Dispatch>; }) => { const { para, index, runParaError, textValueEditor, handleKeyPress } = props; @@ -98,7 +100,7 @@ export const ParaInput = (props: { const renderVisInput = () => { const [isModalOpen, setIsModalOpen] = useState(false); - const [selectableOptions, setSelectableOptions] = useState([]); + const [selectableOptions, setSelectableOptions] = useState([]); const [selectableError, setSelectableError] = useState(false); const onSelect = () => { @@ -108,12 +110,16 @@ export const ParaInput = (props: { return; } props.setIsOutputStale(true); + if (selectedOptions.length > 0) props.setVisType(selectedOptions[0].className); props.setSelectedVisOption(selectedOptions); setIsModalOpen(false); }; - const renderOption = (option, searchValue) => { - const visURL = `visualize#/edit/${option.key}?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'${props.startTime}',to:'${props.endTime}'))`; + 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}`; + } return ( {option.label} @@ -132,6 +138,7 @@ export const ParaInput = (props: { options={props.visOptions} selectedOptions={props.selectedVisOption} onChange={(newOption: EuiComboBoxOptionOption[]) => { + if (newOption.length > 0) props.setVisType(newOption[0].className); props.setSelectedVisOption(newOption); props.setIsOutputStale(true); }} @@ -142,7 +149,10 @@ export const ParaInput = (props: { { - setSelectableOptions(props.visOptions); + setSelectableOptions([ + ...props.visOptions[0].options, + ...props.visOptions[1].options, + ]); setSelectableError(false); setIsModalOpen(true); }} @@ -207,7 +217,11 @@ export const ParaInput = (props: { setIsModalOpen(false)}>Cancel - onSelect()} fill> + onSelect()} + fill + > Select diff --git a/public/components/notebooks/components/paragraph_components/para_output.tsx b/public/components/notebooks/components/paragraph_components/para_output.tsx index 172b5d20d..113e4c536 100644 --- a/public/components/notebooks/components/paragraph_components/para_output.tsx +++ b/public/components/notebooks/components/paragraph_components/para_output.tsx @@ -7,7 +7,10 @@ import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui'; import MarkdownRender from '@nteract/markdown'; import { Media } from '@nteract/outputs'; import moment from 'moment'; +import { VisualizationContainer } from '../../../../components/custom_panels/panel_modules/visualization_container'; +import PPLService from '../../../../services/requests/ppl'; import React, { useState } from 'react'; +import { CoreStart } from '../../../../../../../src/core/public'; import { DashboardContainerInput, DashboardStart, @@ -26,6 +29,8 @@ import { QueryDataGridMemo } from './para_query_grid'; * https://components.nteract.io/#outputs */ export const ParaOutput = (props: { + http: CoreStart['http']; + pplService: PPLService; para: ParaType; visInput: DashboardContainerInput; setVisInput: (input: DashboardContainerInput) => void; @@ -70,6 +75,7 @@ export const ParaOutput = (props: { * Currently supports HTML, TABLE, IMG * TODO: add table rendering */ + const dateFormat = uiSettingsService.get('dateFormat'); if (typeOut !== undefined) { switch (typeOut) { @@ -106,7 +112,6 @@ export const ParaOutput = (props: { ); case 'VISUALIZATION': - const dateFormat = uiSettingsService.get('dateFormat'); let from = moment(visInput?.timeRange?.from).format(dateFormat); let to = moment(visInput?.timeRange?.to).format(dateFormat); from = from === 'Invalid date' ? visInput.timeRange.from : from; @@ -123,6 +128,32 @@ export const ParaOutput = (props: { /> ); + case 'OBSERVABILITY_VISUALIZATION': + let fromObs = moment(visInput?.timeRange?.from).format(dateFormat); + let toObs = moment(visInput?.timeRange?.to).format(dateFormat); + fromObs = fromObs === 'Invalid date' ? visInput.timeRange.from : fromObs; + toObs = toObs === 'Invalid date' ? visInput.timeRange.to : toObs; + return ( + <> + + {`${fromObs} - ${toObs}`} + +
+ +
+ + ); case 'HTML': return ( @@ -144,14 +175,11 @@ export const ParaOutput = (props: { const { para, DashboardContainerByValueRenderer, visInput, setVisInput } = props; - return ( - !para.isOutputHidden ? ( - <> - {para.typeOut.map((typeOut: string, tIdx: number) => { - return outputBody(para.uniqueId + '_paraOutputBody', typeOut, para.out[tIdx]) - } - )} - - ) : null - ); + return !para.isOutputHidden ? ( + <> + {para.typeOut.map((typeOut: string, tIdx: number) => { + return outputBody(para.uniqueId + '_paraOutputBody', typeOut, para.out[tIdx]); + })} + + ) : null; }; diff --git a/public/components/notebooks/components/paragraph_components/paragraphs.tsx b/public/components/notebooks/components/paragraph_components/paragraphs.tsx index 6ca37f430..954c82b49 100644 --- a/public/components/notebooks/components/paragraph_components/paragraphs.tsx +++ b/public/components/notebooks/components/paragraph_components/paragraphs.tsx @@ -6,6 +6,7 @@ import { EuiButton, EuiButtonIcon, + EuiComboBoxOptionOption, EuiContextMenu, EuiContextMenuPanelDescriptor, EuiFlexGroup, @@ -29,11 +30,18 @@ import { } from '../../../../../../../src/plugins/dashboard/public'; import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; import { NOTEBOOKS_API_PREFIX } from '../../../../../common/constants/notebooks'; -import { UI_DATE_FORMAT } from '../../../../../common/constants/shared'; +import { + PPL_DOCUMENTATION_URL, + SQL_DOCUMENTATION_URL, + UI_DATE_FORMAT, +} from '../../../../../common/constants/shared'; import { ParaType } from '../../../../../common/types/notebooks'; import { uiSettingsService } from '../../../../../common/utils'; import { ParaInput } from './para_input'; import { ParaOutput } from './para_output'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; +import PPLService from '../../../../services/requests/ppl'; +import _ from 'lodash'; /* * "Paragraphs" component is used to render cells of the notebook open and "add para div" between paragraphs @@ -60,6 +68,7 @@ import { ParaOutput } from './para_output'; * https://components.nteract.io/#cell */ type ParagraphProps = { + pplService: PPLService; para: ParaType; setPara: (para: ParaType) => void; dateModified: string; @@ -75,7 +84,7 @@ type ParagraphProps = { selectedViewId: string; setSelectedViewId: (viewId: string, scrollToIndex?: number) => void; deletePara: (para: ParaType, index: number) => void; - runPara: (para: ParaType, index: number, vizObjectInput?: string) => void; + runPara: (para: ParaType, index: number, vizObjectInput?: string, paraType?: string) => void; clonePara: (para: ParaType, index: number) => void; movePara: (index: number, targetIndex: number) => void; showQueryParagraphError: boolean; @@ -84,25 +93,24 @@ type ParagraphProps = { export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { const { + pplService, para, index, paragraphSelector, textValueEditor, handleKeyPress, - addPara, DashboardContainerByValueRenderer, - deleteVizualization, showQueryParagraphError, queryParagraphErrorMessage, http, } = props; - const [visOptions, setVisOptions] = useState([]); // options for loading saved visualizations + const [visOptions, setVisOptions] = useState([]); // options for loading saved visualizations const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [runParaError, setRunParaError] = useState(false); - const [selectedVisOption, setSelectedVisOption] = useState([]); + const [selectedVisOption, setSelectedVisOption] = useState([]); const [visInput, setVisInput] = useState(undefined); - const [toggleVisEdit, setToggleVisEdit] = useState(false); + const [visType, setVisType] = useState(''); // output is available if it's not cleared and vis paragraph has a selected visualization const isOutputAvailable = @@ -115,27 +123,57 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { }, })); + const fetchVisualizations = async () => { + let opt1: EuiComboBoxOptionOption[] = []; + let opt2: EuiComboBoxOptionOption[] = []; + await http + .get(`${NOTEBOOKS_API_PREFIX}/visualizations`) + .then((res) => { + opt1 = res.savedVisualizations.map((vizObject) => ({ + label: vizObject.label, + key: vizObject.key, + className: 'VISUALIZATION', + })); + }) + .catch((err) => console.error('Fetching dashboard visualization issue', err.body.message)); + + await http + .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations`) + .then((res) => { + opt2 = res.visualizations.map((vizObject) => ({ + label: vizObject.name, + key: vizObject.id, + className: 'OBSERVABILITY_VISUALIZATION', + })); + }) + .catch((err) => + console.error('Fetching observability visualization issue', err.body.message) + ); + + const allVisualizations = [ + { label: 'Dashboards Visualizations', options: opt1 }, + { label: 'Observability Visualizations', options: opt2 }, + ]; + setVisOptions(allVisualizations); + + const selectedObject = _.filter([...opt1, ...opt2], { + key: para.visSavedObjId, + }); + if (selectedObject.length > 0) { + setVisType(selectedObject.className); + setSelectedVisOption(selectedObject); + } + }; + useEffect(() => { if (para.isVizualisation) { if (para.visSavedObjId !== '') setVisInput(JSON.parse(para.vizObjectInput)); - - http - .get(`${NOTEBOOKS_API_PREFIX}/visualizations`) - .then((res) => { - const opt = res.savedVisualizations.map((vizObject) => ({ - label: vizObject.label, - key: vizObject.key, - })); - setVisOptions(opt); - setSelectedVisOption(opt.filter((o) => o.key === para.visSavedObjId)); - }) - .catch((err) => console.error('Fetching visualization issue', err.body.message)); + fetchVisualizations(); } }, []); - const createNewVizObject = (objectId: string) => { + const createDashboardVizObject = (objectId: string) => { const vizUniqueId = htmlIdGenerator()(); - // a dashboard container object for new visualization const newVizObject: DashboardContainerInput = { viewMode: ViewMode.VIEW, @@ -186,13 +224,13 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { } let newVisObjectInput = undefined; if (para.isVizualisation) { - const inputTemp = createNewVizObject(selectedVisOption[0].key); + const inputTemp = createDashboardVizObject(selectedVisOption[0].key); setVisInput(inputTemp); setRunParaError(false); newVisObjectInput = JSON.stringify(inputTemp); } setRunParaError(false); - return props.runPara(para, index, newVisObjectInput); + return props.runPara(para, index, newVisObjectInput, visType); }; const setStartTime = (time: string) => { @@ -214,6 +252,8 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { // do not show output if it is a visualization paragraph and visInput is not loaded yet const paraOutput = (!para.isVizualisation || visInput) && ( { }; const sqlIcon = ( - + {' '} SQL {' '} ); const pplIcon = ( - + {' '} PPL @@ -446,14 +486,14 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { const queryErrorMessage = queryParagraphErrorMessage.includes('SQL') ? ( {queryParagraphErrorMessage}. Learn More{' '} - + ) : ( {queryParagraphErrorMessage}.{' '} - + Learn More @@ -466,10 +506,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { return ( <> - {renderParaHeader( - para.isVizualisation ? 'OpenSearch Dashboards visualization' : 'Code block', - index - )} + {renderParaHeader(!para.isVizualisation ? 'Code block' : 'Visualization', index)}
paragraphSelector(index)}> {para.isInputExpanded && ( <> @@ -494,6 +531,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { visOptions={visOptions} selectedVisOption={selectedVisOption} setSelectedVisOption={setSelectedVisOption} + setVisType={setVisType} /> {runParaError && ( diff --git a/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap b/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap index 011b8cad0..d6961dce8 100644 --- a/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap +++ b/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap @@ -488,9 +488,19 @@ exports[`Search bar components renders search bar 1`] = `
Id of paragraph to be updated * paragraphInput -> Input to be added */ - updateParagraphInput = function ( + updateParagraph = function ( paragraphs: Array, paragraphId: string, - paragraphInput: string + paragraphInput: string, + paragraphType?: string ) { try { const updatedParagraphs: DefaultParagraph[] = []; @@ -294,6 +295,9 @@ export class DefaultBackend implements NotebookAdaptor { if (paragraph.id === paragraphId) { updatedParagraph.dateModified = new Date().toISOString(); updatedParagraph.input.inputText = paragraphInput; + if (paragraphType.length > 0) { + updatedParagraph.input.inputType = paragraphType; + } } updatedParagraphs.push(updatedParagraph); }); @@ -310,6 +314,9 @@ export class DefaultBackend implements NotebookAdaptor { if (inputType === 'VISUALIZATION') { paragraphType = 'VISUALIZATION'; } + if (inputType === 'OBSERVABILITY_VISUALIZATION') { + paragraphType = 'OBSERVABILITY_VISUALIZATION'; + } if (paragraphInput.substring(0, 3) === '%sql' || paragraphInput.substring(0, 3) === '%ppl') { paragraphType = 'QUERY'; } @@ -386,6 +393,15 @@ export class DefaultBackend implements NotebookAdaptor { execution_time: `${(now() - startTime).toFixed(3)} ms`, }, ]; + } else if (paragraphs[index].input.inputType === 'OBSERVABILITY_VISUALIZATION') { + updatedParagraph.dateModified = new Date().toISOString(); + updatedParagraph.output = [ + { + outputType: 'OBSERVABILITY_VISUALIZATION', + result: '', + execution_time: `${(now() - startTime).toFixed(3)} ms`, + }, + ]; } else if (formatNotRecognized(paragraphs[index].input.inputText)) { updatedParagraph.output = [ { @@ -413,7 +429,7 @@ export class DefaultBackend implements NotebookAdaptor { * paragraphInput -> paragraph input code */ updateRunFetchParagraph = async function ( - client: ILegacyClusterClient, + client: ILegacyScopedClusterClient, request: any, _wreckOptions: optionsType ) { @@ -421,10 +437,11 @@ export class DefaultBackend implements NotebookAdaptor { const scopedClient = client.asScoped(request); const params = request.body; const opensearchClientGetResponse = await this.getNote(scopedClient, params.noteId); - const updatedInputParagraphs = this.updateParagraphInput( + const updatedInputParagraphs = this.updateParagraph( opensearchClientGetResponse.notebook.paragraphs, params.paragraphId, - params.paragraphInput + params.paragraphInput, + params.paragraphType ); const updatedOutputParagraphs = await this.runParagraph( updatedInputParagraphs, @@ -468,7 +485,7 @@ export class DefaultBackend implements NotebookAdaptor { ) { try { const opensearchClientGetResponse = await this.getNote(client, params.noteId); - const updatedInputParagraphs = this.updateParagraphInput( + const updatedInputParagraphs = this.updateParagraph( opensearchClientGetResponse.notebook.paragraphs, params.paragraphId, params.paragraphInput diff --git a/server/adaptors/notebooks/notebook_adaptor.ts b/server/adaptors/notebooks/notebook_adaptor.ts index ed7ee9f93..0ede5ffb2 100644 --- a/server/adaptors/notebooks/notebook_adaptor.ts +++ b/server/adaptors/notebooks/notebook_adaptor.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ILegacyClusterClient, ILegacyScopedClusterClient } from '../../../../../src/core/server'; +import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; import { optionsType } from '../../../common/types/notebooks'; export interface NotebookAdaptor { @@ -85,7 +85,7 @@ export interface NotebookAdaptor { * paragraphInput -> paragraph input code */ updateRunFetchParagraph: ( - client: ILegacyClusterClient, + client: ILegacyScopedClusterClient, request: any, wreckOptions: optionsType ) => Promise; diff --git a/server/adaptors/notebooks/zeppelin_backend.ts b/server/adaptors/notebooks/zeppelin_backend.ts index 105c876ac..1c2bb1b88 100644 --- a/server/adaptors/notebooks/zeppelin_backend.ts +++ b/server/adaptors/notebooks/zeppelin_backend.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RequestHandlerContext } from '../../../../../src/core/server'; +import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; import { optionsType } from '../../../common/types/notebooks'; import { requestor } from '../../common/helpers/notebooks/wreck_requests'; import { NotebookAdaptor } from './notebook_adaptor'; @@ -13,7 +13,7 @@ export class ZeppelinBackend implements NotebookAdaptor { // Gets all the notebooks available from Zeppelin Server // ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook - viewNotes = async function (_context: RequestHandlerContext, wreckOptions: optionsType) { + viewNotes = async function (client: ILegacyScopedClusterClient, wreckOptions: optionsType) { try { let output = []; const body = await requestor('GET', 'api/notebook/', wreckOptions); @@ -29,7 +29,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId] */ fetchNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, noteId: string, wreckOptions: optionsType ) { @@ -46,7 +46,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook */ addNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { name: string }, wreckOptions: optionsType ) { @@ -65,7 +65,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId]/rename */ renameNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { name: string; noteId: string }, wreckOptions: optionsType ) { @@ -88,7 +88,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook/[noteId] */ cloneNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { name: string; noteId: string }, wreckOptions: optionsType ) { @@ -106,7 +106,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook */ deleteNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, noteId: string, wreckOptions: optionsType ) { @@ -123,7 +123,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook/export/{noteid} */ exportNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, noteId: string, wreckOptions: optionsType ) { @@ -140,7 +140,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * ZS Endpoint => http://[zeppelin-server]:[zeppelin-port]/api/notebook/import */ importNote = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, noteObj: any, wreckOptions: optionsType ) { @@ -165,6 +165,7 @@ export class ZeppelinBackend implements NotebookAdaptor { params: { paragraphIndex: number; noteId: string; paragraphInput: string; inputType: string } ) { const visualizationPrefix = '%sh #vizobject:'; + const observabilityVisualizationPrefix = '%sh #observabilityviz:'; let paragraphText = params.paragraphInput; if ( @@ -174,6 +175,13 @@ export class ZeppelinBackend implements NotebookAdaptor { paragraphText = visualizationPrefix + paragraphText; } + if ( + params.inputType === 'OBSERVABILITY_VISUALIZATION' && + params.paragraphInput.substring(0, 22) !== observabilityVisualizationPrefix + ) { + paragraphText = visualizationPrefix + paragraphText; + } + if (params.paragraphInput === '') { paragraphText = '%md\n'; } @@ -205,11 +213,12 @@ export class ZeppelinBackend implements NotebookAdaptor { */ updateParagraph = async function ( wreckOptions: optionsType, - params: { noteId: string; paragraphId: string; paragraphInput: string } + params: { noteId: string; paragraphId: string; paragraphInput: string; paragraphType?: string } ) { wreckOptions.payload = { text: params.paragraphInput, }; + if (params.paragraphType !== undefined) wreckOptions.payload.title = params.paragraphType; try { const body = await requestor( 'PUT', @@ -308,10 +317,11 @@ export class ZeppelinBackend implements NotebookAdaptor { * paragraphInput -> paragraph input code */ updateRunFetchParagraph = async function ( - _context: RequestHandlerContext, - params: { noteId: string; paragraphId: string; paragraphInput: string }, + client: ILegacyScopedClusterClient, + request: any, wreckOptions: optionsType ) { + const params = request.params; try { const updateInfo = await this.updateParagraph(wreckOptions, params); const runInfo = await this.runPara(wreckOptions, params); @@ -329,7 +339,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * paragraphInput -> paragraph input code */ updateFetchParagraph = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { noteId: string; paragraphId: string; paragraphInput: string }, wreckOptions: optionsType ) { @@ -348,7 +358,7 @@ export class ZeppelinBackend implements NotebookAdaptor { * paragraphId -> Id of the paragraph to be fetched */ addFetchNewParagraph = async function ( - _context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { noteId: string; paragraphIndex: number; paragraphInput: string; inputType: string }, wreckOptions: optionsType ) { @@ -368,13 +378,13 @@ export class ZeppelinBackend implements NotebookAdaptor { * paragraphId -> Id of the paragraph to be deleted */ deleteFetchParagraphs = async function ( - context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { noteId: string; paragraphId: string }, wreckOptions: optionsType ) { try { const delinfo = await this.deleteParagraph(wreckOptions, params); - const notebookinfo = await this.fetchNote(context, params.noteId, wreckOptions); + const notebookinfo = await this.fetchNote(client, params.noteId, wreckOptions); return { paragraphs: notebookinfo }; } catch (error) { throw new Error('Delete Paragraph Error:' + error); @@ -386,13 +396,13 @@ export class ZeppelinBackend implements NotebookAdaptor { * Param: noteId -> Id of notebook to be cleared */ clearAllFetchParagraphs = async function ( - context: RequestHandlerContext, + client: ILegacyScopedClusterClient, params: { noteId: string }, wreckOptions: optionsType ) { try { const clearinfo = await this.clearAllParagraphs(wreckOptions, params.noteId); - const notebookinfo = await this.fetchNote(context, params.noteId, wreckOptions); + const notebookinfo = await this.fetchNote(client, params.noteId, wreckOptions); return { paragraphs: notebookinfo }; } catch (error) { throw new Error('Clear Paragraph Error:' + error); diff --git a/server/routes/notebooks/paraRouter.ts b/server/routes/notebooks/paraRouter.ts index fca99cc6d..bf7d17577 100644 --- a/server/routes/notebooks/paraRouter.ts +++ b/server/routes/notebooks/paraRouter.ts @@ -30,6 +30,7 @@ export function registerParaRoute(router: IRouter) { noteId: schema.string(), paragraphId: schema.string(), paragraphInput: schema.string(), + paragraphType: schema.string(), }), }, }, diff --git a/test/jest.config.js b/test/jest.config.js index 744e57372..dc778aa5f 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -25,6 +25,6 @@ module.exports = { moduleNameMapper: { '\\.(css|less|sass|scss)$': '/test/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg|png)$': '/test/__mocks__/fileMock.js', - '\\@algolia/autocomplete-theme-classic$': '/test/__mocks__/styleMock.js' + '\\@algolia/autocomplete-theme-classic$': '/test/__mocks__/styleMock.js', }, -}; +}; \ No newline at end of file diff --git a/test/setup.jest.ts b/test/setup.jest.ts index 51915b139..e321d2c02 100644 --- a/test/setup.jest.ts +++ b/test/setup.jest.ts @@ -18,3 +18,5 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ return () => 'random_html_id'; }, })); + +jest.setTimeout(30000);