From a985d35417954fff2caf49a5db7aedbc5fd2d54d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Oct 2023 22:48:05 +0000 Subject: [PATCH] Reverting notebooks changes, add docs/validation to datasources, explorer minor fixes (#1101) * Add documentation linking and validation for query permissions Signed-off-by: Derek Ho * Update test with link Signed-off-by: Derek Ho * Add form validation for name field Signed-off-by: Derek Ho * reverting notebook changes from #974 & #985 Signed-off-by: Shenoy Pratik * Remove console log Signed-off-by: Derek Ho * surrounding events functionally working Signed-off-by: Paul Sebastian * small surrounding flyout ui changes Signed-off-by: Paul Sebastian --------- Signed-off-by: Derek Ho Signed-off-by: Shenoy Pratik Signed-off-by: Paul Sebastian Co-authored-by: Shenoy Pratik Co-authored-by: Paul Sebastian (cherry picked from commit 4a8b500ef3cb8dea004d66dca4cc607a8c00ceda) Signed-off-by: github-actions[bot] --- common/constants/data_connections.ts | 3 +- common/types/data_connections.ts | 1 + ...anage_data_connections_table.test.tsx.snap | 2 +- .../components/new/configure_datasource.tsx | 23 +- .../new/configure_prometheus_datasource.tsx | 36 +- .../new/configure_s3_datasource.tsx | 37 +- .../datasources/components/new/name_row.tsx | 68 ++++ .../components/new/query_permissions.tsx | 37 +- .../explorer/events_views/data_grid.tsx | 6 + .../explorer/events_views/docViewRow.tsx | 20 +- .../events_views/surrounding_flyout.tsx | 106 ++++-- .../event_analytics/utils/utils.tsx | 4 +- .../__tests__/sampleZeppelinNotebooks.tsx | 359 ++++++++++++++++++ .../__tests__/zeppelin_parser.test.tsx | 44 +++ .../components/helpers/default_parser.tsx | 79 ++-- .../components/helpers/zeppelin_parser.tsx | 149 ++++++++ .../notebooks/components/notebook.tsx | 43 ++- .../__snapshots__/para_output.test.tsx.snap | 4 +- .../__snapshots__/paragraphs.test.tsx.snap | 2 +- .../paragraph_components/para_output.tsx | 85 ++--- .../paragraph_components/paragraphs.tsx | 1 + public/components/notebooks/index.scss | 5 - 22 files changed, 913 insertions(+), 201 deletions(-) create mode 100644 public/components/datasources/components/new/name_row.tsx create mode 100644 public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx create mode 100644 public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx create mode 100644 public/components/notebooks/components/helpers/zeppelin_parser.tsx diff --git a/common/constants/data_connections.ts b/common/constants/data_connections.ts index 570c551e4..04aac36ed 100644 --- a/common/constants/data_connections.ts +++ b/common/constants/data_connections.ts @@ -5,7 +5,8 @@ import { DatasourceType } from '../../common/types/data_connections'; -export const OPENSEARCH_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/data-sources/index'; +export const OPENSEARCH_DOCUMENTATION_URL = + 'https://opensearch.org/docs/latest/dashboards/management/data-sources/'; export const OPENSEARCH_ACC_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/data-acceleration/index'; diff --git a/common/types/data_connections.ts b/common/types/data_connections.ts index 0c9228c2d..a246f662c 100644 --- a/common/types/data_connections.ts +++ b/common/types/data_connections.ts @@ -10,6 +10,7 @@ export interface PermissionsConfigurationProps { selectedRoles: Role[]; setSelectedRoles: React.Dispatch>; layout: 'horizontal' | 'vertical'; + hasSecurityAccess: boolean; } export type Role = EuiComboBoxOptionOption; diff --git a/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap index 679611ccc..b1450c9e4 100644 --- a/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap +++ b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap @@ -36,7 +36,7 @@ exports[`Manage Data Connections Table test Renders manage data connections tabl diff --git a/public/components/datasources/components/new/configure_datasource.tsx b/public/components/datasources/components/new/configure_datasource.tsx index 9b4f6ed4b..244ab0c4b 100644 --- a/public/components/datasources/components/new/configure_datasource.tsx +++ b/public/components/datasources/components/new/configure_datasource.tsx @@ -40,6 +40,7 @@ export function Configure(props: ConfigureDatasourceProps) { const { type, notifications } = props; const { http, chrome } = coreRefs; const { setToast } = useToast(); + const [error, setError] = useState(''); const [authMethod, setAuthMethod] = useState('basicauth'); const [name, setName] = useState(''); const [details, setDetails] = useState(''); @@ -51,6 +52,7 @@ export function Configure(props: ConfigureDatasourceProps) { const [secretKey, setSecretKey] = useState(''); const [region, setRegion] = useState(''); const [roles, setRoles] = useState([]); + const [hasSecurityAccess, setHasSecurityAccess] = useState(true); const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState([]); const [page, setPage] = useState<'configure' | 'review'>('configure'); const ConfigureDatasourceSteps = [ @@ -64,13 +66,16 @@ export function Configure(props: ConfigureDatasourceProps) { ]; useEffect(() => { - http!.get('/api/v1/configuration/roles').then((data) => - setRoles( - Object.keys(data.data).map((key) => { - return { label: key }; - }) + http! + .get('/api/v1/configuration/roles') + .then((data) => + setRoles( + Object.keys(data.data).map((key) => { + return { label: key }; + }) + ) ) - ); + .catch((err) => setHasSecurityAccess(false)); chrome!.setBreadcrumbs([ { text: 'Data sources', @@ -110,6 +115,9 @@ export function Configure(props: ConfigureDatasourceProps) { setPasswordForRequest={setPassword} currentAuthMethod={authMethod} setAuthMethodForRequest={setAuthMethod} + hasSecurityAccess={hasSecurityAccess} + error={error} + setError={setError} /> ); case 'PROMETHEUS': @@ -136,6 +144,9 @@ export function Configure(props: ConfigureDatasourceProps) { setRegionForRequest={setRegion} currentAuthMethod={authMethod} setAuthMethodForRequest={setAuthMethod} + hasSecurityAccess={hasSecurityAccess} + error={error} + setError={setError} /> ); default: diff --git a/public/components/datasources/components/new/configure_prometheus_datasource.tsx b/public/components/datasources/components/new/configure_prometheus_datasource.tsx index b63155ebf..6f4ecb4f0 100644 --- a/public/components/datasources/components/new/configure_prometheus_datasource.tsx +++ b/public/components/datasources/components/new/configure_prometheus_datasource.tsx @@ -24,6 +24,7 @@ import { import { QueryPermissionsConfiguration } from './query_permissions'; import { Role } from '../../../../../common/types/data_connections'; import { AuthDetails } from './auth_details'; +import { NameRow } from './name_row'; interface ConfigurePrometheusDatasourceProps { roles: Role[]; @@ -38,6 +39,9 @@ interface ConfigurePrometheusDatasourceProps { currentSecretKey: string; currentRegion: string; currentAuthMethod: AuthMethod; + hasSecurityAccess: boolean; + error: string; + setError: React.Dispatch>; setAuthMethodForRequest: React.Dispatch>; setRegionForRequest: React.Dispatch>; setAccessKeyForRequest: React.Dispatch>; @@ -72,9 +76,11 @@ export const ConfigurePrometheusDatasource = (props: ConfigurePrometheusDatasour setRegionForRequest, currentAuthMethod, setAuthMethodForRequest, + hasSecurityAccess, + error, + setError, } = props; - const [name, setName] = useState(currentName); const [details, setDetails] = useState(currentDetails); const [store, setStore] = useState(currentStore); const authOptions = [ @@ -101,27 +107,12 @@ export const ConfigurePrometheusDatasource = (props: ConfigurePrometheusDatasour

Data source details

- - <> - -

- Connection name that OpenSearch Dashboards references. This name should be - descriptive and concise. -

-
- { - setName(e.target.value); - }} - onBlur={(e) => { - setNameForRequest(e.target.value); - }} - /> - -
+ diff --git a/public/components/datasources/components/new/configure_s3_datasource.tsx b/public/components/datasources/components/new/configure_s3_datasource.tsx index 3e17c3e86..f35cc4d7d 100644 --- a/public/components/datasources/components/new/configure_s3_datasource.tsx +++ b/public/components/datasources/components/new/configure_s3_datasource.tsx @@ -22,6 +22,7 @@ import { import { QueryPermissionsConfiguration } from './query_permissions'; import { Role } from '../../../../../common/types/data_connections'; import { AuthDetails } from './auth_details'; +import { NameRow } from './name_row'; interface ConfigureS3DatasourceProps { roles: Role[]; @@ -34,6 +35,9 @@ interface ConfigureS3DatasourceProps { currentAuthMethod: AuthMethod; currentUsername: string; currentPassword: string; + hasSecurityAccess: boolean; + error: string; + setError: React.Dispatch>; setAuthMethodForRequest: React.Dispatch>; setPasswordForRequest: React.Dispatch>; setUsernameForRequest: React.Dispatch>; @@ -62,9 +66,11 @@ export const ConfigureS3Datasource = (props: ConfigureS3DatasourceProps) => { currentUsername, setPasswordForRequest, setUsernameForRequest, + hasSecurityAccess, + error, + setError, } = props; - const [name, setName] = useState(currentName); const [details, setDetails] = useState(currentDetails); const [arn, setArn] = useState(currentArn); const [store, setStore] = useState(currentStore); @@ -91,27 +97,13 @@ export const ConfigureS3Datasource = (props: ConfigureS3DatasourceProps) => {

Data source details

- - <> - -

- Connection name that OpenSearch Dashboards references. This name should be - descriptive and concise. -

-
- { - setName(e.target.value); - }} - onBlur={(e) => { - setNameForRequest(e.target.value); - }} - /> - -
+ { selectedRoles={selectedQueryPermissionRoles} setSelectedRoles={setSelectedQueryPermissionRoles} layout={'vertical'} + hasSecurityAccess={hasSecurityAccess} /> diff --git a/public/components/datasources/components/new/name_row.tsx b/public/components/datasources/components/new/name_row.tsx new file mode 100644 index 000000000..96c96aaa4 --- /dev/null +++ b/public/components/datasources/components/new/name_row.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiText, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { coreRefs } from '../../../../../public/framework/core_refs'; + +interface ConfigureNameProps { + currentName: string; + currentError: string; + setErrorForForm: React.Dispatch>; + setNameForRequest: React.Dispatch>; +} + +export const NameRow = (props: ConfigureNameProps) => { + const { setNameForRequest, currentName, currentError, setErrorForForm } = props; + const { pplService } = coreRefs; + + const [name, setName] = useState(currentName); + const [existingNames, setExistingNames] = useState([]); + + useEffect(() => { + pplService!.fetch({ query: 'show datasources', format: 'jdbc' }).then((dataconnections) => + setExistingNames( + dataconnections.jsonData.map((x) => { + return x.DATASOURCE_NAME; + }) + ) + ); + }, []); + + const onBlur = (e) => { + if (e.target.value === '') { + setErrorForForm('Name is a required parameter.'); + } else if (existingNames.includes(e.target.value)) { + setErrorForForm('Name must be unique across data sources.'); + } else { + setErrorForForm(''); + } + + setNameForRequest(e.target.value); + }; + + return ( + + <> + +

+ Connection name that OpenSearch Dashboards references. This name should be descriptive + and concise. +

+
+ { + setName(e.target.value); + }} + onBlur={onBlur} + isInvalid={currentError.length !== 0} + /> + +
+ ); +}; diff --git a/public/components/datasources/components/new/query_permissions.tsx b/public/components/datasources/components/new/query_permissions.tsx index 67ec9e9d8..d5a899c08 100644 --- a/public/components/datasources/components/new/query_permissions.tsx +++ b/public/components/datasources/components/new/query_permissions.tsx @@ -7,21 +7,17 @@ import { EuiComboBox, EuiFlexGroup, EuiFlexItem, - EuiLink, + EuiFormRow, EuiRadioGroup, EuiSpacer, EuiText, } from '@elastic/eui'; import React, { useState } from 'react'; -import { - OPENSEARCH_DOCUMENTATION_URL, - QUERY_ALL, - QUERY_RESTRICTED, -} from '../../../../../common/constants/data_connections'; +import { QUERY_ALL, QUERY_RESTRICTED } from '../../../../../common/constants/data_connections'; import { PermissionsConfigurationProps } from '../../../../../common/types/data_connections'; export const QueryPermissionsConfiguration = (props: PermissionsConfigurationProps) => { - const { roles, selectedRoles, setSelectedRoles, layout } = props; + const { roles, selectedRoles, setSelectedRoles, layout, hasSecurityAccess } = props; const [selectedAccessLevel, setSelectedAccessLevel] = useState( selectedRoles.length ? QUERY_RESTRICTED : QUERY_ALL @@ -30,6 +26,7 @@ export const QueryPermissionsConfiguration = (props: PermissionsConfigurationPro { id: QUERY_RESTRICTED, label: 'Restricted - accessible by users with specific OpenSearch roles', + disabled: !hasSecurityAccess, }, { id: QUERY_ALL, @@ -45,14 +42,24 @@ export const QueryPermissionsConfiguration = (props: PermissionsConfigurationPro Select one or more OpenSearch roles that can query this data connection. - + + + ); }; diff --git a/public/components/event_analytics/explorer/events_views/data_grid.tsx b/public/components/event_analytics/explorer/events_views/data_grid.tsx index 8ac9c7aa9..f6019d42b 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.tsx +++ b/public/components/event_analytics/explorer/events_views/data_grid.tsx @@ -143,6 +143,12 @@ export function DataGrid(props: DataGridProps) { pplService={pplService} rawQuery={rawQuery} onFlyoutOpen={() => {}} + dataGridColumns={dataGridColumns} + dataGridColumnVisibility={dataGridColumnVisibility} + selectedIndex={rowIndex} + sortingFields={sortingFields} + rowHeightsOptions={rowHeightsOptions} + rows={rows} /> ); }, diff --git a/public/components/event_analytics/explorer/events_views/docViewRow.tsx b/public/components/event_analytics/explorer/events_views/docViewRow.tsx index bd0a69e88..07830e10f 100644 --- a/public/components/event_analytics/explorer/events_views/docViewRow.tsx +++ b/public/components/event_analytics/explorer/events_views/docViewRow.tsx @@ -34,6 +34,12 @@ interface FlyoutButtonProps { pplService: PPLService; rawQuery: string; onFlyoutOpen: (docId: string) => void; + dataGridColumns: any; + dataGridColumnVisibility: any; + selectedIndex: any; + sortingFields: any; + rowHeightsOptions: any; + rows: any; } export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { @@ -47,6 +53,12 @@ export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { pplService, rawQuery, onFlyoutOpen, + dataGridColumns, + dataGridColumnVisibility, + selectedIndex, + sortingFields, + rowHeightsOptions, + rows, } = props; const [detailsOpen, setDetailsOpen] = useState(false); @@ -237,6 +249,12 @@ export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { getTds={getTds} toggleSize={flyoutToggleSize} setToggleSize={setFlyoutToggleSize} + dataGridColumns={dataGridColumns} + dataGridColumnVisibility={dataGridColumnVisibility} + selectedIndex={selectedIndex} + sortingFields={sortingFields} + rowHeightsOptions={rowHeightsOptions} + rows={rows} /> ); }, [ @@ -272,7 +290,7 @@ export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { <> toggleDetailOpen()} - iconType={'inspect'} + iconType={detailsOpen || surroundingEventsOpen ? 'minimize' : 'inspect'} aria-label="inspect document details" /> {flyout} 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 e7fe8d989..7b1eafe92 100644 --- a/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx +++ b/public/components/event_analytics/explorer/events_views/surrounding_flyout.tsx @@ -4,7 +4,7 @@ */ import './docView.scss'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, Fragment } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -19,25 +19,23 @@ import { EuiSpacer, EuiText, EuiTitle, - EuiToolTip, + EuiDataGrid, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, } from '@elastic/eui'; +import moment from 'moment'; import { FlyoutContainers } from '../../../common/flyout_containers'; import { IDocType } from './docViewRow'; -import { IExplorerFields, IField } from '../../../../../common/types/explorer'; -import { getHeaders, fetchSurroundingData, rangeNumDocs, populateDataGrid } from '../../utils'; -import { DEFAULT_COLUMNS } from '../../../../../common/constants/explorer'; -import { HttpSetup } from '../../../../../../../src/core/public'; +import { IField } from '../../../../../common/types/explorer'; +import { fetchSurroundingData, rangeNumDocs } from '../../utils'; +import { DATE_DISPLAY_FORMAT } from '../../../../../common/constants/explorer'; import PPLService from '../../../../services/requests/ppl'; interface Props { - http: HttpSetup; - detailsOpen: boolean; setDetailsOpen: React.Dispatch>; doc: IDocType; timeStampField: string; - memorizedTds: JSX.Element[]; - explorerFields: IExplorerFields; - openTraces: boolean; setOpenTraces: React.Dispatch>; setSurroundingEventsOpen: React.Dispatch>; pplService: PPLService; @@ -46,17 +44,17 @@ interface Props { getTds: (doc: IDocType, selectedCols: IField[], isFlyout: boolean) => JSX.Element[]; toggleSize: boolean; setToggleSize: React.Dispatch>; + dataGridColumns: any; + dataGridColumnVisibility: any; + sortingFields: any; + rowHeightsOptions: any; + rows: any; } export const SurroundingFlyout = ({ - http, - detailsOpen, setDetailsOpen, doc, timeStampField, - memorizedTds, - explorerFields, - openTraces, setOpenTraces, setSurroundingEventsOpen, pplService, @@ -65,6 +63,10 @@ export const SurroundingFlyout = ({ getTds, toggleSize, setToggleSize, + dataGridColumns, + dataGridColumnVisibility, + sortingFields, + rowHeightsOptions, }: Props) => { const [numNewEvents, setNumNewEvents] = useState(5); const [valueOldEvents, setNumOldEvents] = useState(5); @@ -72,8 +74,8 @@ export const SurroundingFlyout = ({ const [loadingOldEvents, setLoadingOldEvents] = useState(false); const [oldEventsError, setOldEventsError] = useState(''); const [newEventsError, setNewEventsError] = useState(''); - const [newEventsData, setNewEventsData] = useState([[]]); - const [oldEventsData, setOldEventsData] = useState([[]]); + const [newEventsData, setNewEventsData] = useState([]); + const [oldEventsData, setOldEventsData] = useState([]); const closeFlyout = () => { setDetailsOpen(false); @@ -123,6 +125,46 @@ export const SurroundingFlyout = ({ } }; + const renderCells = ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + let actualIndex: number; + let rowDoc: any; + + if (rowIndex < newEventsData.length) { + // within newEvents section of table, pull data from there + actualIndex = rowIndex; + rowDoc = newEventsData[rowIndex]; + } else if (rowIndex === newEventsData.length) { + // is the selected row + actualIndex = rowIndex; + rowDoc = doc; + } else if (rowIndex > newEventsData.length) { + // within oldEvents section of table + actualIndex = rowIndex - (newEventsData.length + 1); + rowDoc = oldEventsData[actualIndex]; + } else { + throw Error(); + } + + if (columnId === '_source') { + return ( + + {Object.keys(rowDoc).map((key) => ( + + + {key} + + {rowDoc[key]} + + ))} + + ); + } + if (columnId === 'timestamp') { + return `${moment(rowDoc[columnId]).format(DATE_DISPLAY_FORMAT)}`; + } + return `${rowDoc[columnId]}`; + }; + const loadButton = (typeOfDocs: 'new' | 'old') => { if (typeOfDocs === 'new') { loadData(typeOfDocs, numNewEvents + 5); @@ -229,7 +271,7 @@ export const SurroundingFlyout = ({ const flyoutBody = ( -
+
{getInputForm('arrowUp', onChangeNewEvents, numNewEvents, 'new')}
@@ -237,21 +279,17 @@ export const SurroundingFlyout = ({ )}
- {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 !== '' && ( diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index 3266b51f2..d680860e1 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -94,7 +94,7 @@ export const fetchSurroundingData = async ( eventTime: string, numDocs: number, typeOfDocs: 'new' | 'old', - setEventsData: React.Dispatch>, + setEventsData: React.Dispatch>, setIsError: React.Dispatch>, setLoadingData: React.Dispatch>, selectedCols: IField[], @@ -123,7 +123,7 @@ export const fetchSurroundingData = async ( .then((res) => { const resuleData = typeOfDocs === 'new' ? res.jsonData.reverse() : res.jsonData; resultCount = resuleData.length; - setEventsData(createTds(resuleData, selectedCols, getTds)); + setEventsData(resuleData); }) .catch((error: Error) => { setIsError(error.message); diff --git a/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx b/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx new file mode 100644 index 000000000..db07da1c5 --- /dev/null +++ b/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx @@ -0,0 +1,359 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Sample notebook with all input and output +export const sampleNotebook1 = { + paragraphs: [ + { + text: + "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", + user: 'anonymous', + dateUpdated: '2020-08-20 21:15:04.590', + config: {}, + settings: { params: {}, forms: {} }, + results: { + code: 'SUCCESS', + msg: [ + { + type: 'HTML', + data: + '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', + }, + ], + }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958104590_901298942', + id: 'paragraph_1596519508360_932236116', + dateCreated: '2020-08-20 21:15:04.590', + status: 'READY', + }, + { + title: 'VISUALIZATION', + text: + '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + user: 'anonymous', + dateUpdated: '2020-08-20 21:25:28.588', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958728587_1310320520', + id: 'paragraph_1597958728587_1310320520', + dateCreated: '2020-08-20 21:25:28.587', + status: 'READY', + }, + ], + name: 'Embed Vizualization', + id: '2FJH8PW8K', + defaultInterpreterGroup: 'spark', + version: '0.9.0-preview2', + noteParams: {}, + noteForms: {}, + angularObjects: {}, + config: { isZeppelinNotebookCronEnable: false }, + info: {}, +}; + +// Parsed Output of sample notebook1 +export const sampleParsedParagraghs1 = [ + { + uniqueId: 'paragraph_1596519508360_932236116', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: false, + vizObjectInput: '', + id: 1, + inp: + "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", + lang: 'text/x-', + editorLanguage: '', + typeOut: ['HTML'], + out: [ + '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', + ], + }, + { + uniqueId: 'paragraph_1597958728587_1310320520', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: true, + vizObjectInput: + '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + id: 2, + inp: + '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + lang: 'text/x-', + editorLanguage: '', + typeOut: [], + out: [], + }, +]; + +// Sample notebook with all input and cleared outputs +export const sampleNotebook2 = { + paragraphs: [ + { + text: + "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", + user: 'anonymous', + dateUpdated: '2020-08-20 21:15:04.590', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958104590_901298942', + id: 'paragraph_1596519508360_932236116', + dateCreated: '2020-08-20 21:15:04.590', + status: 'READY', + }, + { + title: 'Paragraph inserted', + text: '%md\n\n## Greetings!\n* Yay! you may import and export me ', + user: 'anonymous', + dateUpdated: '2020-08-20 21:15:04.590', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958104590_1715920734', + id: 'paragraph_1596742076640_674206137', + dateCreated: '2020-08-20 21:15:04.590', + status: 'READY', + }, + { + title: 'Paragraph inserted', + text: + "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", + user: 'anonymous', + dateUpdated: '2020-08-20 21:15:04.590', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958104590_931410594', + id: 'paragraph_1596524302932_2112910756', + dateCreated: '2020-08-20 21:15:04.590', + status: 'READY', + }, + { + title: 'VISUALIZATION', + text: + '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + user: 'anonymous', + dateUpdated: '2020-08-20 21:25:28.588', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958728587_1310320520', + id: 'paragraph_1597958728587_1310320520', + dateCreated: '2020-08-20 21:25:28.587', + status: 'READY', + }, + ], + name: 'Embed Vizualization', + id: '2FJH8PW8K', + defaultInterpreterGroup: 'spark', + version: '0.9.0-preview2', + noteParams: {}, + noteForms: {}, + angularObjects: {}, + config: { isZeppelinNotebookCronEnable: false }, + info: {}, +}; + +// Parsed Output of sample notebook2 +export const sampleParsedParagraghs2 = [ + { + uniqueId: 'paragraph_1596519508360_932236116', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: false, + vizObjectInput: '', + id: 1, + inp: + "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", + lang: 'text/x-', + editorLanguage: '', + typeOut: [], + out: [], + }, + { + uniqueId: 'paragraph_1596742076640_674206137', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: false, + vizObjectInput: '', + id: 2, + inp: '%md\n\n## Greetings!\n* Yay! you may import and export me ', + lang: 'text/x-md', + editorLanguage: 'md', + typeOut: [], + out: [], + }, + { + uniqueId: 'paragraph_1596524302932_2112910756', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: false, + vizObjectInput: '', + id: 3, + inp: + "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", + lang: 'text/x-md', + editorLanguage: 'md', + typeOut: [], + out: [], + }, + { + uniqueId: 'paragraph_1597958728587_1310320520', + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: true, + vizObjectInput: + '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + id: 4, + inp: + '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + lang: 'text/x-', + editorLanguage: '', + typeOut: [], + out: [], + }, +]; + +// Sample notebook with no paragraph Id +export const sampleNotebook3 = { + paragraphs: [ + { + text: + "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", + user: 'anonymous', + dateUpdated: '2020-08-20 21:15:04.590', + config: {}, + settings: { params: {}, forms: {} }, + results: { + code: 'SUCCESS', + msg: [ + { + type: 'HTML', + data: + '
\n

Hi Everyone

\n
    \n
  • Here’s a demo on OpenSearch Dashboards Notebooks
  • \n
  • You may use the top left buttons to play around with notebooks and Paragraphs
  • \n
\n\n
', + }, + ], + }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958104590_901298942', + dateCreated: '2020-08-20 21:15:04.590', + status: 'READY', + }, + ], + name: 'Embed Vizualization', + id: '2FJH8PW8K', + defaultInterpreterGroup: 'spark', + version: '0.9.0-preview2', + noteParams: {}, + noteForms: {}, + angularObjects: {}, + config: { isZeppelinNotebookCronEnable: false }, + info: {}, +}; + +// Sample notebook with no VISUALIZAITON title +export const sampleNotebook4 = { + paragraphs: [ + { + text: + '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', + user: 'anonymous', + dateUpdated: '2020-08-20 21:25:28.588', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958728587_1310320520', + id: 'paragraph_1597958728587_1310320520', + dateCreated: '2020-08-20 21:25:28.587', + status: 'READY', + }, + ], + name: 'Embed Vizualization', + id: '2FJH8PW8K', + defaultInterpreterGroup: 'spark', + version: '0.9.0-preview2', + noteParams: {}, + noteForms: {}, + angularObjects: {}, + config: { isZeppelinNotebookCronEnable: false }, + info: {}, +}; + +// Sample notebook with no input and output +export const sampleNotebook5 = { + paragraphs: [ + { + user: 'anonymous', + dateUpdated: '2020-08-20 21:25:28.588', + config: {}, + settings: { params: {}, forms: {} }, + apps: [], + runtimeInfos: {}, + progressUpdateIntervalMs: 500, + jobName: 'paragraph_1597958728587_1310320520', + id: 'paragraph_1597958728587_1310320520', + dateCreated: '2020-08-20 21:25:28.587', + status: 'READY', + }, + ], + name: 'Embed Vizualization', + id: '2FJH8PW8K', + defaultInterpreterGroup: 'spark', + version: '0.9.0-preview2', + noteParams: {}, + noteForms: {}, + angularObjects: {}, + config: { isZeppelinNotebookCronEnable: false }, + info: {}, +}; diff --git a/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx b/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx new file mode 100644 index 000000000..e6c5b351e --- /dev/null +++ b/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { zeppelinParagraphParser } from '../zeppelin_parser'; +import { + sampleNotebook1, + sampleNotebook2, + sampleNotebook3, + sampleNotebook4, + sampleNotebook5, + sampleParsedParagraghs1, + sampleParsedParagraghs2, +} from './sampleZeppelinNotebooks'; + +// Perfect schema +describe('Testing Zeppelin backend parser function with perfect schema', () => { + test('zeppelinParagraphParserTest1', (done) => { + const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook1.paragraphs); + const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook2.paragraphs); + const parsedParagraphs3 = zeppelinParagraphParser([]); + expect(parsedParagraphs1).toEqual(sampleParsedParagraghs1); + expect(parsedParagraphs2).toEqual(sampleParsedParagraghs2); + expect(parsedParagraphs3).toEqual([]); + done(); + }); +}); + +// Issue in schema +describe('Testing default backend parser function with wrong schema', () => { + test('zeppelinParagraphParserTest2', (done) => { + expect(() => { + const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook3.paragraphs); + }).toThrow(Error); + expect(() => { + const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook4.paragraphs); + }).toThrow(Error); + expect(() => { + const parsedParagraphs3 = zeppelinParagraphParser(sampleNotebook5.paragraphs); + }).toThrow(Error); + done(); + }); +}); diff --git a/public/components/notebooks/components/helpers/default_parser.tsx b/public/components/notebooks/components/helpers/default_parser.tsx index 7647d80f3..c38dad3cd 100644 --- a/public/components/notebooks/components/helpers/default_parser.tsx +++ b/public/components/notebooks/components/helpers/default_parser.tsx @@ -3,13 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ParaType } from '../../../../../common/types/notebooks'; + // Get the type of output and result in a default notebook paragraph // Param: Default Backend Paragraph const parseOutput = (paraObject: any) => { try { + let outputType = []; + let result = []; + paraObject.output.map((output: { outputType: string; result: string }) => { + outputType.push(output.outputType); + result.push(output.result); + }); return { - outputType: paraObject.output.map(({ outputType }) => outputType), - outputData: paraObject.output.map(({ result }) => result), + outputType: outputType, + outputData: result, }; } catch (error) { return { @@ -38,7 +46,7 @@ const parseInputType = (paraObject: any) => { const parseVisualization = (paraObject: any) => { try { if (paraObject.input.inputType.includes('VISUALIZATION')) { - const vizContent = paraObject.input.inputText; + let vizContent = paraObject.input.inputText; const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); let visStartTime = startDate.toISOString(); @@ -68,42 +76,43 @@ const parseVisualization = (paraObject: any) => { } }; -const parseBackendParagraph = (paraObject, index) => { - const codeLanguage = parseInputType(paraObject); - const vizParams = parseVisualization(paraObject); - const message = parseOutput(paraObject); - - return { - uniqueId: paraObject.id, - isRunning: false, - inQueue: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: vizParams.isViz, - vizObjectInput: vizParams.VizObject, - id: index + 1, - inp: paraObject.input.inputText || '', - lang: 'text/x-' + codeLanguage, - editorLanguage: codeLanguage, - typeOut: message.outputType, - out: message.outputData, - isInputExpanded: false, - isOutputStale: false, - paraRef: undefined, - paraDivRef: undefined, - visStartTime: vizParams.visStartTime, - visEndTime: vizParams.visEndTime, - visSavedObjId: vizParams.visSavedObjId, - }; -}; - // Placeholder for default parser // Param: Default Backend Paragraph export const defaultParagraphParser = (defaultBackendParagraphs: any) => { + let parsedPara: Array = []; try { - return defaultBackendParagraphs.map(parseBackendParagraph); + defaultBackendParagraphs.map((paraObject: any, index: number) => { + const codeLanguage = parseInputType(paraObject); + const vizParams = parseVisualization(paraObject); + const message = parseOutput(paraObject); + + let tempPara: ParaType = { + uniqueId: paraObject.id, + isRunning: false, + inQueue: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: vizParams.isViz, + vizObjectInput: vizParams.VizObject, + id: index + 1, + inp: paraObject.input.inputText || '', + lang: 'text/x-' + codeLanguage, + editorLanguage: codeLanguage, + typeOut: message.outputType, + out: message.outputData, + isInputExpanded: false, + isOutputStale: false, + paraRef: undefined, + paraDivRef: undefined, + visStartTime: vizParams.visStartTime, + visEndTime: vizParams.visEndTime, + visSavedObjId: vizParams.visSavedObjId, + }; + parsedPara.push(tempPara); + }); + return parsedPara; } catch (error) { throw new Error('Parsing Paragraph Issue ' + error); } diff --git a/public/components/notebooks/components/helpers/zeppelin_parser.tsx b/public/components/notebooks/components/helpers/zeppelin_parser.tsx new file mode 100644 index 000000000..f2293c3a7 --- /dev/null +++ b/public/components/notebooks/components/helpers/zeppelin_parser.tsx @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* This file contains parsing functions + * These functions have to be changed based on backend configuration + * If backend changes the incoming paragraph structures may change, so parsing adapts to it + */ + +import { ParaType } from '../../../common'; + +const visualizationPrefix = '%sh #vizobject:'; +const observabilityVisualizationPrefix = '%sh #observabilityviz:'; + +const langSupport = { + '%sh': 'shell', + '%md': 'md', + '%python': 'python', + '%opensearchsql': 'sql', + '%elasticsearch': 'json', +}; + +// Get the coding language from a Zeppelin paragraph input +// Param: textHeader-> header on a Zeppelin paragraph example "%md" +const parseCodeLanguage = (textHeader: string) => { + const codeLanguage = langSupport[textHeader]; + return codeLanguage || ''; +}; + +// Get the type of output message from a Zeppelin paragraph +// Param: Zeppelin Paragraph +const parseMessage = (paraObject: any) => { + try { + let mtype = []; + let mdata = []; + paraObject.results.msg.map((msg: { type: string; data: string }) => { + mtype.push(msg.type); + mdata.push(msg.data); + }); + return { + outputType: mtype, + outputData: mdata, + }; + } catch (error) { + return { + outputType: [], + outputData: [], + }; + } +}; + +// Get the type of output message from a Zeppelin paragraph +// Param: Zeppelin Paragraph +const parseText = (paraObject: any) => { + if ('text' in paraObject) { + return paraObject.text; + } else { + throw new Error('Input text parse issue'); + } +}; + +// Get the visualization from a Zeppelin Paragraph input +// All Visualizations in Zeppelin are stored as shell comment -> "%sh #vizobject:" +// TODO: This is a workaround need to look for better solutions +// Param: Zeppelin Paragraph +const parseVisualization = (paraObject: any) => { + let vizContent = ''; + if ( + paraObject.hasOwnProperty('text') && + paraObject.text.substring(0, 15) === visualizationPrefix + ) { + if (paraObject.title !== 'VISUALIZATION') { + throw new Error('Visualization parse issue'); + } + vizContent = paraObject.text.substring(15); + return { + isViz: true, + VizObject: vizContent, + }; + } + + 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: true, + VizObject: vizContent, + }; + } + + return { + isViz: false, + VizObject: vizContent, + }; +}; + +// This parser is used to get paragraph id +// Param: Zeppelin Paragraph +const parseId = (paraObject: any) => { + if ('id' in paraObject) { + return paraObject.id; + } else { + throw new Error('Id not found in paragraph'); + } +}; + +// This parser helps to convert Zeppelin paragraphs to a common ParaType format +// This parsing makes any backend notebook compatible with notebooks plugin +export const zeppelinParagraphParser = (zeppelinBackendParagraphs: any) => { + let parsedPara: Array = []; + try { + zeppelinBackendParagraphs.map((paraObject: ParaType, index: number) => { + const paragraphId = parseId(paraObject); + const vizParams = parseVisualization(paraObject); + const inputParam = parseText(paraObject); + const codeLanguage = parseCodeLanguage(inputParam.split('\n')[0].split('.')[0]); + const message = parseMessage(paraObject); + + let tempPara = { + uniqueId: paragraphId, + isRunning: false, + inQueue: false, + ishovered: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: vizParams.isViz, + vizObjectInput: vizParams.VizObject, + id: index + 1, + inp: inputParam, + lang: 'text/x-' + codeLanguage, + editorLanguage: codeLanguage, + typeOut: message.outputType, + out: message.outputData, + }; + parsedPara.push(tempPara); + }); + return parsedPara; + } catch (error) { + throw new Error('Parsing Paragraph Issue ' + error); + } +}; diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index 9a7d720a7..e5494ebed 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -30,7 +30,11 @@ import { RouteComponentProps } from 'react-router-dom'; import PPLService from '../../../services/requests/ppl'; import { ChromeBreadcrumb, CoreStart } from '../../../../../../src/core/public'; import { DashboardStart } from '../../../../../../src/plugins/dashboard/public'; -import { CREATE_NOTE_MESSAGE, NOTEBOOKS_API_PREFIX } from '../../../../common/constants/notebooks'; +import { + CREATE_NOTE_MESSAGE, + NOTEBOOKS_API_PREFIX, + NOTEBOOKS_SELECTED_BACKEND, +} from '../../../../common/constants/notebooks'; import { UI_DATE_FORMAT } from '../../../../common/constants/shared'; import { ParaType } from '../../../../common/types/notebooks'; import { GenerateReportLoadingModal } from './helpers/custom_modals/reporting_loading_modal'; @@ -41,6 +45,7 @@ import { contextMenuViewReports, generateInContextReport, } from './helpers/reporting_context_menu_helper'; +import { zeppelinParagraphParser } from './helpers/zeppelin_parser'; import { Paragraphs } from './paragraph_components/paragraphs'; const panelStyles: CSS.Properties = { float: 'left', @@ -85,6 +90,7 @@ interface NotebookState { dateModified: string; paragraphs: any; // notebook paragraphs fetched from API parsedPara: ParaType[]; // paragraphs parsed to a common format + vizPrefix: string; // prefix for visualizations in Zeppelin Adaptor isAddParaPopoverOpen: boolean; isParaActionsPopoverOpen: boolean; isNoteActionsPopoverOpen: boolean; @@ -106,6 +112,7 @@ export class Notebook extends Component { dateModified: '', paragraphs: [], parsedPara: [], + vizPrefix: '', isAddParaPopoverOpen: false, isParaActionsPopoverOpen: false, isNoteActionsPopoverOpen: false, @@ -131,12 +138,20 @@ export class Notebook extends Component { // parse paragraphs based on backend parseParagraphs = (paragraphs: any[]): ParaType[] => { try { - return defaultParagraphParser(paragraphs).map((para) => ({ - ...para, - isInputExpanded: this.state.selectedViewId === 'input_only', - paraRef: React.createRef(), - paraDivRef: React.createRef(), - })); + let parsedPara; + // @ts-ignore + if (NOTEBOOKS_SELECTED_BACKEND === 'ZEPPELIN') { + parsedPara = zeppelinParagraphParser(paragraphs); + this.setState({ vizPrefix: '%sh #vizobject:' }); + } else { + parsedPara = defaultParagraphParser(paragraphs); + } + parsedPara.forEach((para: ParaType) => { + para.isInputExpanded = this.state.selectedViewId === 'input_only'; + para.paraRef = React.createRef(); + para.paraDivRef = React.createRef(); + }); + return parsedPara; } catch (err) { this.props.setToast( 'Error parsing paragraphs, please make sure you have the correct permission.', @@ -169,7 +184,8 @@ export class Notebook extends Component { paragraphSelector = (index: number) => { const parsedPara = this.state.parsedPara; this.state.parsedPara.map((_: ParaType, idx: number) => { - parsedPara[idx].isSelected = index === idx; + if (index === idx) parsedPara[idx].isSelected = true; + else parsedPara[idx].isSelected = false; }); this.setState({ parsedPara }); }; @@ -455,13 +471,22 @@ export class Notebook extends Component { }; // Backend call to update and run contents of paragraph - updateRunParagraph = (para: ParaType, index: number) => { + updateRunParagraph = ( + para: ParaType, + index: number, + vizObjectInput?: string, + paraType?: string + ) => { this.showParagraphRunning(index); + if (vizObjectInput) { + para.inp = this.state.vizPrefix + vizObjectInput; // "%sh check" + } const paraUpdateObject = { noteId: this.props.openedNoteId, paragraphId: para.uniqueId, paragraphInput: para.inp, + paragraphType: paraType || '', }; return this.props.http diff --git a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap index a149d56be..a56281a10 100644 --- a/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap +++ b/public/components/notebooks/components/paragraph_components/__tests__/__snapshots__/para_output.test.tsx.snap @@ -2,7 +2,7 @@ exports[` spec renders markdown outputs 1`] = `
spec renders other types of outputs 1`] = ` exports[` spec renders query outputs 1`] = `
select * from opensearch_dashboards_sample_data_flights limit 2 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 bc5be1116..0fb456810 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 @@ -275,7 +275,7 @@ exports[` spec renders the component 1`] = ` style="opacity: 1; padding: 15px;" >
{ const createQueryColumns = (jsonColumns: any[]) => { let index = 0; - const datagridColumns = []; + let datagridColumns = []; for (index = 0; index < jsonColumns.length; ++index) { const datagridColumnObject = { id: jsonColumns[index].name, @@ -54,7 +54,7 @@ export const ParaOutput = (props: { let index = 0; let schemaIndex = 0; for (index = 0; index < queryObject.datarows.length; ++index) { - const datarowValue = {}; + let datarowValue = {}; for (schemaIndex = 0; schemaIndex < queryObject.schema.length; ++schemaIndex) { const columnName = queryObject.schema[schemaIndex].name; if (typeof queryObject.datarows[index][schemaIndex] === 'object') { @@ -70,34 +70,7 @@ export const ParaOutput = (props: { return data; }; - const QueryOutput = ({ typeOut, val }: { typeOut: string; val: string }) => { - const inputQuery = para.inp.substring(4, para.inp.length); - const queryObject = JSON.parse(val); - const columns = createQueryColumns(queryObject.schema); - const data = getQueryOutputData(queryObject); - const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id)); - if (queryObject.hasOwnProperty('error')) { - return {val}; - } else { - return ( -
- - {inputQuery} - - - -
- ); - } - }; - - const OutputBody = ({ typeOut, val }: { typeOut: string; val: string }) => { + const outputBody = (key: string, typeOut: string, val: string) => { /* Returns a component to render paragraph outputs using the para.typeOut property * Currently supports HTML, TABLE, IMG * TODO: add table rendering @@ -107,10 +80,34 @@ export const ParaOutput = (props: { if (typeOut !== undefined) { switch (typeOut) { case 'QUERY': - return ; + const inputQuery = para.inp.substring(4, para.inp.length); + const queryObject = JSON.parse(val); + if (queryObject.hasOwnProperty('error')) { + return {val}; + } else { + const columns = createQueryColumns(queryObject.schema); + const data = getQueryOutputData(queryObject); + const [visibleColumns, setVisibleColumns] = useState(() => columns.map(({ id }) => id)); + return ( +
+ + {inputQuery} + + + +
+ ); + } case 'MARKDOWN': return ( - + ); @@ -124,7 +121,11 @@ export const ParaOutput = (props: { {`${from} - ${to}`} - + ); case 'OBSERVABILITY_VISUALIZATION': @@ -159,17 +160,17 @@ export const ParaOutput = (props: { ); case 'HTML': return ( - + {/* eslint-disable-next-line react/jsx-pascal-case */} ); case 'TABLE': - return
{val}
; + return
{val}
; case 'IMG': - return ; + return ; default: - return
{val}
; + return
{val}
; } } else { console.log('output not supported', typeOut); @@ -182,13 +183,7 @@ export const ParaOutput = (props: { return !para.isOutputHidden ? ( <> {para.typeOut.map((typeOut: string, tIdx: number) => { - return ( - - ); + 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 d9ad6b7de..8a1e16297 100644 --- a/public/components/notebooks/components/paragraph_components/paragraphs.tsx +++ b/public/components/notebooks/components/paragraph_components/paragraphs.tsx @@ -256,6 +256,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => {