+
+
+
+
+
+

+ No paragraphs +

+
+ Add a paragraph to compose your document or story. Notebooks now support two types of input: +
+
+
+
+
+
+
+
+
+ +
+
+ + Query + +
+

+ Write contents directly using markdown, SQL or PPL. +

+
+
+ +
+
+
+
+
+ +
+
+ + Visualization + +
+

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

+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap b/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap deleted file mode 100644 index 741eaca7..00000000 --- a/public/components/notebooks/components/__tests__/__snapshots__/notebook.test.tsx.snap +++ /dev/null @@ -1,394 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` spec Renders the empty component 1`] = ` -
-
-
-
-
-

- sample-notebook-1 -

-
-
-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-

- Created on 12/14/2023 06:49 PM -

-
-
-
-
-
-
-
-
-
-
-

- No paragraphs -

-
- Add a paragraph to compose your document or story. Notebooks now support two types of input: -
-
-
-
-
-
-
-
-
- -
-
- - Query - -
-

- Write contents directly using markdown, SQL or PPL. -

-
-
- -
-
-
-
-
- -
-
- - Visualization - -
-

- 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__/classic_notebook.test.tsx similarity index 95% rename from public/components/notebooks/components/__tests__/notebook.test.tsx rename to public/components/notebooks/components/__tests__/classic_notebook.test.tsx index 2108eb09..08c51aa2 100644 --- a/public/components/notebooks/components/__tests__/notebook.test.tsx +++ b/public/components/notebooks/components/__tests__/classic_notebook.test.tsx @@ -20,8 +20,9 @@ import { notebookPutResponse, runCodeBlockResponse, } from '../../../../../test/notebooks_constants'; -import { Notebook, NotebookProps } from '../notebook'; +import { ClassicNotebook, ClassicNotebookProps } from '../classic_notebook'; import { + applicationServiceMock, chromeServiceMock, notificationServiceMock, savedObjectsServiceMock, @@ -138,7 +139,8 @@ jest.mock('../../../../../public/services', () => ({ })), })); -const ContextAwareNotebook = (props: NotebookProps & { dataSourceEnabled?: boolean }) => { +const ContextAwareNotebook = (props: ClassicNotebookProps & { dataSourceEnabled?: boolean }) => { + const applicationMock = applicationServiceMock.createStartContract(); return (
, }), }, - updateContext: jest.fn(), - findingService: { - initialize: jest.fn(), + application: { + ...applicationMock, + capabilities: { + ...applicationMock.capabilities, + investigation: { + ...applicationMock.capabilities.investigation, + agenticFeaturesEnabled: true, + }, + }, }, }} > - + ); }; @@ -189,7 +197,7 @@ describe(' spec', () => { const history = jest.fn() as any; history.replace = jest.fn(); history.push = jest.fn(); - const defaultProps: NotebookProps = { + const defaultProps: ClassicNotebookProps = { openedNoteId: '458e1320-3f05-11ef-bd29-e58626f102c0', showPageHeader: true, }; diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/agentic_notebook.tsx similarity index 51% rename from public/components/notebooks/components/notebook.tsx rename to public/components/notebooks/components/agentic_notebook.tsx index c93cad12..53bdf9d8 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/agentic_notebook.tsx @@ -4,7 +4,6 @@ */ import { - EuiCallOut, EuiCard, EuiEmptyPrompt, EuiFlexGroup, @@ -19,31 +18,24 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import CSS from 'csstype'; -import React, { useState, useRef, useEffect } from 'react'; - -import { useContext } from 'react'; +import React, { useState, useRef, useEffect, useCallback, useContext } from 'react'; import { useEffectOnce, useObservable } from 'react-use'; -import { useCallback } from 'react'; import { NoteBookServices } from 'public/types'; import { ParagraphState } from '../../../../common/state/paragraph_state'; import { - CREATE_NOTE_MESSAGE, - DEEP_RESEARCH_PARAGRAPH_TYPE, - NOTEBOOKS_API_PREFIX, -} from '../../../../common/constants/notebooks'; -import { NoteBookSource } from '../../../../common/types/notebooks'; -import { getCustomModal, getDeleteModal } from './helpers/modal_containers'; + NotebookComponentProps, + NoteBookSource, + NotebookType, +} from '../../../../common/types/notebooks'; +import { getDeleteModal } from './helpers/modal_containers'; import { Paragraphs } from './paragraph_components/paragraphs'; import { NotebookContextProvider, NotebookReactContext, getDefaultState, } from '../context_provider/context_provider'; -import { InputPanel } from './input_panel'; import { useParagraphs } from '../../../hooks/use_paragraphs'; -import { isValidUUID } from './helpers/notebooks_parser'; import { useNotebook } from '../../../hooks/use_notebook'; import { usePrecheck } from '../../../hooks/use_precheck'; import { useNotebookFindingIntegration } from '../../../hooks/use_notebook_finding_integration'; @@ -58,28 +50,13 @@ import { HypothesisDetail } from './hypothesis/hypothesis_detail'; import { HypothesesPanel } from './hypothesis/hypotheses_panel'; import { SubRouter, useSubRouter } from '../../../hooks/use_sub_router'; -const panelStyles: CSS.Properties = { - marginTop: '10px', -}; - -interface NotebookComponentProps { - showPageHeader?: boolean; -} - -/* - * "Notebook" component is used to display an open notebook - * - * Props taken in as params are: - * DashboardContainerByValueRenderer - Dashboard container renderer for visualization - * http object - for making API requests - */ -export interface NotebookProps extends NotebookComponentProps { +interface AgenticNotebookProps extends NotebookComponentProps { openedNoteId: string; } -export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { +function NotebookComponent({ showPageHeader }: NotebookComponentProps) { const { - services: { http, notifications, findingService, chrome, assistantDashboards }, + services: { notifications, findingService, chrome, assistantDashboards }, } = useOpenSearchDashboards(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -91,11 +68,11 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { useContextSubscription(); const notebookContext = useContext(NotebookReactContext); - const { source } = useObservable( + const { source, notebookType } = useObservable( notebookContext.state.value.context.getValue$(), notebookContext.state.value.context.value ); - const { id: openedNoteId, paragraphs: paragraphsStates, path, isLoading } = useObservable( + const { id: openedNoteId, paragraphs: paragraphsStates, isLoading } = useObservable( notebookContext.state.getValue$(), notebookContext.state.value ); @@ -103,7 +80,6 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { notebookContext.state.value.context.getValue$(), notebookContext.state.value.context.value ); - const isSavedObjectNotebook = isValidUUID(openedNoteId); const paraDivRefs = useRef>([]); const { isInvestigating, doInvestigate, addNewFinding } = useInvestigation(); @@ -133,58 +109,6 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { setIsModalVisible(true); }; - const migrateNotebook = async ( - migrateNoteName: string, - migrateNoteID: string - ): Promise => { - if (migrateNoteName.length >= 50 || migrateNoteName.length === 0) { - notifications.toasts.addDanger('Invalid notebook name'); - return Promise.reject(); - } - const migrateNoteObject = { - name: migrateNoteName, - noteId: migrateNoteID, - }; - return http - .post(`${NOTEBOOKS_API_PREFIX}/note/migrate`, { - body: JSON.stringify(migrateNoteObject), - }) - .then((res) => { - notifications.toasts.addSuccess(`Notebook "${migrateNoteName}" successfully created!`); - return res.id; - }) - .catch((err) => { - notifications.toasts.addDanger( - 'Error migrating notebook, please make sure you have the correct permission.' - ); - console.error(err.body.message); - }); - }; - - const showUpgradeModal = () => { - setModalLayout( - getCustomModal( - (newName: string) => { - migrateNotebook(newName, openedNoteId).then((id: string) => { - window.location.assign(`#/${id}`); - setTimeout(() => { - loadNotebook(); - }, 300); - }); - setIsModalVisible(false); - }, - () => setIsModalVisible(false), - 'Name', - 'Upgrade notebook', - 'Cancel', - 'Upgrade', - path + ' (upgraded)', - CREATE_NOTE_MESSAGE - ) - ); - setIsModalVisible(true); - }; - const scrollToPara = useCallback((index: number) => { setTimeout(() => { window.scrollTo({ @@ -195,10 +119,6 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { }, 0); }, []); - const handleInputPanelParagraphCreated = useCallback(() => { - scrollToPara(paraDivRefs.current.length - 1); - }, [scrollToPara]); - const loadNotebook = useCallback(() => { loadNotebookHook() .then(async (res) => { @@ -243,35 +163,36 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { loadNotebook(); // TODO: remove the optional chain after each method - chrome.setIsNavDrawerLocked?.(false); - assistantDashboards?.updateChatbotVisible?.(true); + (chrome as any).setIsNavDrawerLocked?.(false); + (assistantDashboards as any)?.updateChatbotVisible?.(true); }); + if (!isLoading && notebookType === NotebookType.CLASSIC) { + return ( + + + Error loading Notebook} + body={

Incorrect notebook type

} + /> +
+
+ ); + } + return ( <> {showPageHeader && ( {}} /> )} - {!isSavedObjectNotebook && ( - - - Upgrade this notebook to take full advantage of the latest features - - showUpgradeModal()} - > - Upgrade Notebook - - - - )} {source === NoteBookSource.DISCOVER && ( <> @@ -316,7 +237,11 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { }) ) : ( // show default paragraph if no paragraphs in this notebook -
+
@@ -327,98 +252,70 @@ export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { - {isSavedObjectNotebook && ( - - - - } - title="Query" - description="Write contents directly using markdown, SQL or PPL." - footer={ - - createParagraph({ - index: 0, - input: { - inputText: '%ppl ', - inputType: 'CODE', - }, - }) - } - style={{ marginBottom: 17 }} - > - Add query - - } - /> - - - } - title="Visualization" - description="Import OpenSearch Dashboards or Observability visualizations to the notes." - footer={ - - createParagraph({ - index: 0, - input: { - inputText: '', - inputType: 'VISUALIZATION', - }, - }) - } - style={{ marginBottom: 17 }} - > - Add visualization - - } - /> - - {initialGoal ? ( - - } - title="Deep Research" - description="Use deep research to analytics question." - footer={ - - createParagraph({ - index: 0, - input: { - inputText: initialGoal, - inputType: DEEP_RESEARCH_PARAGRAPH_TYPE, - }, - }) - } - style={{ marginBottom: 17 }} - > - Add deep research - + + + + } + title="Query" + description="Write contents directly using markdown, SQL or PPL." + footer={ + + createParagraph({ + index: 0, + input: { + inputText: '%ppl ', + inputType: 'CODE', + }, + }) } - /> - - ) : null} - - - )} + style={{ marginBottom: 17 }} + > + Add query + + } + /> + + + } + title="Visualization" + description="Import OpenSearch Dashboards or Observability visualizations to the notes." + footer={ + + createParagraph({ + index: 0, + input: { + inputText: '', + inputType: 'VISUALIZATION', + }, + }) + } + style={{ marginBottom: 17 }} + > + Add visualization + + } + /> + + + +
)} - - {isModalVisible && modalLayout} ); } -export const Notebook = ({ openedNoteId, ...rest }: NotebookProps) => { +export const AgenticNotebook = ({ openedNoteId, ...rest }: AgenticNotebookProps) => { const { services: { dataSource }, } = useOpenSearchDashboards(); diff --git a/public/components/notebooks/components/classic_notebook.tsx b/public/components/notebooks/components/classic_notebook.tsx new file mode 100644 index 00000000..865e0c6f --- /dev/null +++ b/public/components/notebooks/components/classic_notebook.tsx @@ -0,0 +1,337 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiCallOut, + EuiCard, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingContent, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSmallButton, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useState, useEffect, useRef } from 'react'; + +import { useContext } from 'react'; +import { useObservable } from 'react-use'; +import { useCallback } from 'react'; +import { NoteBookServices } from 'public/types'; +import { ParagraphState } from '../../../../common/state/paragraph_state'; +import { CREATE_NOTE_MESSAGE, NOTEBOOKS_API_PREFIX } from '../../../../common/constants/notebooks'; +import { NotebookComponentProps, NotebookType } from '../../../../common/types/notebooks'; +import { getCustomModal, getDeleteModal } from './helpers/modal_containers'; +import { Paragraphs } from './paragraph_components/paragraphs'; +import { + NotebookContextProvider, + NotebookReactContext, + getDefaultState, +} from '../context_provider/context_provider'; +import { InputPanel } from './input_panel'; +import { isValidUUID } from './helpers/notebooks_parser'; +import { useNotebook } from '../../../hooks/use_notebook'; +import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; +import { NotebookHeader } from './notebook_header'; +import { useParagraphs } from '../../../hooks/use_paragraphs'; + +export interface ClassicNotebookProps extends NotebookComponentProps { + openedNoteId: string; +} + +export function NotebookComponent({ showPageHeader }: NotebookComponentProps) { + const { + services: { http, notifications }, + } = useOpenSearchDashboards(); + + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); + const { createParagraph, deleteParagraph } = useParagraphs(); + const { loadNotebook: loadNotebookHook } = useNotebook(); + + const notebookContext = useContext(NotebookReactContext); + const { id: openedNoteId, paragraphs: paragraphsStates, path, isLoading } = useObservable( + notebookContext.state.getValue$(), + notebookContext.state.value + ); + const { notebookType } = useObservable( + notebookContext.state.value.context.getValue$(), + notebookContext.state.value.context.value + ); + const isSavedObjectNotebook = isValidUUID(openedNoteId); + const paraDivRefs = useRef>([]); + + const showDeleteParaModal = (index: number) => { + setModalLayout( + getDeleteModal( + () => setIsModalVisible(false), + () => { + deleteParagraph(index); + setIsModalVisible(false); + }, + 'Delete paragraph', + 'Are you sure you want to delete the paragraph? The action cannot be undone.' + ) + ); + setIsModalVisible(true); + }; + + const migrateNotebook = async ( + migrateNoteName: string, + migrateNoteID: string + ): Promise => { + if (migrateNoteName.length >= 50 || migrateNoteName.length === 0) { + notifications.toasts.addDanger('Invalid notebook name'); + return Promise.reject(); + } + const migrateNoteObject = { + name: migrateNoteName, + noteId: migrateNoteID, + }; + return http + .post(`${NOTEBOOKS_API_PREFIX}/note/migrate`, { + body: JSON.stringify(migrateNoteObject), + }) + .then((res) => { + notifications.toasts.addSuccess(`Notebook "${migrateNoteName}" successfully created!`); + return res.id; + }) + .catch((err) => { + notifications.toasts.addDanger( + 'Error migrating notebook, please make sure you have the correct permission.' + ); + console.error(err.body.message); + }); + }; + + const showUpgradeModal = () => { + setModalLayout( + getCustomModal( + (newName: string) => { + migrateNotebook(newName, openedNoteId).then((id: string) => { + window.location.assign(`#/${id}`); + setTimeout(() => { + loadNotebook(); + }, 300); + }); + setIsModalVisible(false); + }, + () => setIsModalVisible(false), + 'Name', + 'Upgrade notebook', + 'Cancel', + 'Upgrade', + path + ' (upgraded)', + CREATE_NOTE_MESSAGE + ) + ); + setIsModalVisible(true); + }; + + const scrollToPara = useCallback((index: number) => { + setTimeout(() => { + window.scrollTo({ + left: 0, + top: paraDivRefs.current[index]?.offsetTop, + behavior: 'smooth', + }); + }, 0); + }, []); + + const handleInputPanelParagraphCreated = useCallback(() => { + scrollToPara(paraDivRefs.current.length - 1); + }, [scrollToPara]); + + const loadNotebook = useCallback(() => { + loadNotebookHook() + .then(async (res) => { + if (res.context) { + notebookContext.state.updateContext(res.context); + } + notebookContext.state.updateValue({ + dateCreated: res.dateCreated, + path: res.path, + vizPrefix: res.vizPrefix, + paragraphs: res.paragraphs.map((paragraph) => new ParagraphState(paragraph)), + owner: res.owner, + }); + }) + .catch((err) => { + notifications.toasts.addDanger( + 'Error fetching notebooks, please make sure you have the correct permission.' + ); + console.error(err); + }); + }, [loadNotebookHook, notifications.toasts, notebookContext.state]); + + useEffect(() => { + loadNotebook(); + }, [loadNotebook]); + + if (!isLoading && notebookType === NotebookType.AGENTIC) { + return ( + + + Error loading Notebook} + body={

Incorrect notebook type

} + /> +
+
+ ); + } + + return ( + <> + + + {showPageHeader && ( + + )} + {!isSavedObjectNotebook && ( + + + Upgrade this notebook to take full advantage of the latest features + + showUpgradeModal()} + > + Upgrade Notebook + + + + )} + + {isLoading ? ( + } title={

Loading Notebook

} /> + ) : null} + {isLoading ? null : paragraphsStates.length > 0 ? ( + paragraphsStates.map((paragraphState, index: number) => ( +
(paraDivRefs.current[index] = ref)} + key={`para_div_${paragraphState.value.id}`} + > + {index > 0 && } + +
+ )) + ) : ( + // show default paragraph if no paragraphs in this notebook +
+ + + +

No paragraphs

+ + Add a paragraph to compose your document or story. Notebooks now support two + types of input: + +
+ + {isSavedObjectNotebook && ( + + + + } + title="Query" + description="Write contents directly using markdown, SQL or PPL." + footer={ + + createParagraph({ + index: 0, + input: { + inputText: '%ppl ', + inputType: 'CODE', + }, + }) + } + style={{ marginBottom: 17 }} + > + Add query + + } + /> + + + } + title="Visualization" + description="Import OpenSearch Dashboards or Observability visualizations to the notes." + footer={ + + createParagraph({ + index: 0, + input: { + inputText: '', + inputType: 'VISUALIZATION', + }, + }) + } + style={{ marginBottom: 17 }} + > + Add visualization + + } + /> + + + + )} + +
+
+ )} +
+
+ + +
+ {isModalVisible && modalLayout} + + ); +} + +export const ClassicNotebook = ({ openedNoteId, ...rest }: ClassicNotebookProps) => { + const { + services: { dataSource }, + } = useOpenSearchDashboards(); + const stateRef = useRef( + getDefaultState({ + id: openedNoteId, + dataSourceEnabled: !!dataSource, + }) + ); + return ( + + + + ); +}; diff --git a/public/components/notebooks/components/input_panel.tsx b/public/components/notebooks/components/input_panel.tsx index d3bfa1a5..7c6228a7 100644 --- a/public/components/notebooks/components/input_panel.tsx +++ b/public/components/notebooks/components/input_panel.tsx @@ -70,8 +70,6 @@ export const InputPanel: React.FC = ({ onParagraphCreated }) => [paragraphs.length, dataSourceId, createParagraph, runParagraph, onParagraphCreated] ); - return null; - return (
{ path={['/create', '/']} render={(_props) => } /> + { + return ; + }} + /> { - return ; + return ; }} /> diff --git a/public/components/notebooks/components/note_table.tsx b/public/components/notebooks/components/note_table.tsx index 58653132..a711786f 100644 --- a/public/components/notebooks/components/note_table.tsx +++ b/public/components/notebooks/components/note_table.tsx @@ -382,7 +382,15 @@ export function NoteTable({ deleteNotebook }: NoteTableProps) { sortable: true, truncateText: true, render: (value, record) => ( - {truncate(value, { length: 100 })} + + {truncate(value, { length: 100 })} + ), }, { diff --git a/public/plugin.tsx b/public/plugin.tsx index 8733f473..23e86cd5 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -36,7 +36,10 @@ import { setNotifications, FindingService, } from './services'; -import { Notebook, NotebookProps } from './components/notebooks/components/notebook'; +import { + ClassicNotebook, + ClassicNotebookProps, +} from './components/notebooks/components/classic_notebook'; import { NOTEBOOK_APP_NAME } from '../common/constants/notebooks'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; import { paragraphRegistry } from './paragraphs'; @@ -170,12 +173,13 @@ export class InvestigationPlugin } ); - const getNotebook = async ({ openedNoteId }: Pick) => { + // TODO: check if we need to expose agentic notebook + const getNotebook = async ({ openedNoteId }: Pick) => { const services = await getServices(); return ( - + ); }; diff --git a/test/notebooks_constants.ts b/test/notebooks_constants.ts index b6ca46a0..7f87bb50 100644 --- a/test/notebooks_constants.ts +++ b/test/notebooks_constants.ts @@ -34,7 +34,7 @@ export const codeBlockNotebook = { id: 'paragraph_de00ea2d-a8fb-45d1-8085-698f51c6b6be', }, ], - context: { notebookType: 'Agentic' }, + context: { notebookType: 'Classic' }, }; export const migrateBlockNotebook = { @@ -77,7 +77,7 @@ export const emptyNotebook = { dateCreated: '2023-12-14T18:49:43.375Z', dateModified: '2023-12-15T06:13:23.463Z', paragraphs: [], - context: { notebookType: 'Agentic' }, + context: { notebookType: 'Classic' }, }; // Sample notebook with all input and output