mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Search] Introduced Notebooks view for console (#180400)
## Summary This PR adds the search-notebooks plugin and a python notebook renderer to the persistent console. ### Screenshots Console Closed <img width="1418" alt="image" src="8e2e2934
-a19f-4204-8a31-1e8eab7fd20f"> Notebooks: <img width="1418" alt="image" src="bf9d40ad
-352d-482e-8d84-f426c3026c69"> <img width="1418" alt="image" src="fcf8cac2
-4640-49e8-9bce-94a5a853383f"> Console View <img width="1418" alt="image" src="9230d1c2
-3987-41f8-aa86-77a20509b8c0"> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f447ed09c5
commit
308f514a45
56 changed files with 1292 additions and 118 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -490,6 +490,7 @@ src/plugins/interactive_setup @elastic/kibana-security
|
|||
test/interactive_setup_api_integration/plugins/test_endpoints @elastic/kibana-security
|
||||
packages/kbn-interpreter @elastic/kibana-visualizations
|
||||
packages/kbn-io-ts-utils @elastic/obs-knowledge-team
|
||||
packages/kbn-ipynb @elastic/enterprise-search-frontend
|
||||
packages/kbn-jest-serializers @elastic/kibana-operations
|
||||
packages/kbn-journeys @elastic/kibana-operations @elastic/appex-qa
|
||||
packages/kbn-json-ast @elastic/kibana-operations
|
||||
|
|
|
@ -6,6 +6,7 @@ xpack.cloudSecurityPosture.enabled: false
|
|||
xpack.infra.enabled: true
|
||||
xpack.uptime.enabled: true
|
||||
xpack.securitySolution.enabled: false
|
||||
xpack.search.notebooks.enabled: false
|
||||
|
||||
## Enable the slo plugin
|
||||
xpack.slo.enabled: true
|
||||
|
@ -14,7 +15,7 @@ xpack.slo.enabled: true
|
|||
xpack.cloud.serverless.project_type: observability
|
||||
|
||||
## Enable the Serverless Observability plugin
|
||||
xpack.serverless.observability.enabled: true
|
||||
xpack.serverless.observability.enabled: true
|
||||
|
||||
## Configure plugins
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ xpack.infra.enabled: false
|
|||
xpack.observabilityLogsExplorer.enabled: false
|
||||
xpack.observability.enabled: false
|
||||
xpack.observabilityAIAssistant.enabled: false
|
||||
xpack.search.notebooks.enabled: false
|
||||
|
||||
## Cloud settings
|
||||
xpack.cloud.serverless.project_type: security
|
||||
|
|
|
@ -521,6 +521,7 @@
|
|||
"@kbn/interactive-setup-test-endpoints-plugin": "link:test/interactive_setup_api_integration/plugins/test_endpoints",
|
||||
"@kbn/interpreter": "link:packages/kbn-interpreter",
|
||||
"@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils",
|
||||
"@kbn/ipynb": "link:packages/kbn-ipynb",
|
||||
"@kbn/kbn-health-gateway-status-plugin": "link:test/health_gateway/plugins/status",
|
||||
"@kbn/kbn-sample-panel-action-plugin": "link:test/plugin_functional/plugins/kbn_sample_panel_action",
|
||||
"@kbn/kbn-top-nav-plugin": "link:test/plugin_functional/plugins/kbn_top_nav",
|
||||
|
|
3
packages/kbn-ipynb/README.md
Normal file
3
packages/kbn-ipynb/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/ipynb
|
||||
|
||||
This package has a component to render iPython Notebooks.
|
57
packages/kbn-ipynb/components/index.tsx
Normal file
57
packages/kbn-ipynb/components/index.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { NotebookDefinition } from '../types';
|
||||
|
||||
import { NotebookCell } from './notebook_cell';
|
||||
import { NotebookCellOutput } from './notebook_cell_output';
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
notebook: NotebookDefinition;
|
||||
}
|
||||
|
||||
export const NotebookRenderer = ({ notebook }: NotebookRendererProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const language = notebook.metadata?.language_info?.name ?? 'python';
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="xl"
|
||||
style={{ maxWidth: `${euiTheme.base * 50}px` }}
|
||||
justifyContent="center"
|
||||
>
|
||||
{notebook.cells.map((cell, i) => {
|
||||
const cellId = cell.id ?? `nb.cell.${i}`;
|
||||
if (cell.outputs && cell.outputs.length > 0) {
|
||||
return (
|
||||
<EuiFlexItem key={cellId}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<NotebookCell cell={cell} language={language} />
|
||||
</EuiFlexItem>
|
||||
{cell.outputs.map((output, outputIndex) => (
|
||||
<EuiFlexItem key={`${cellId}.output.${outputIndex}`}>
|
||||
<NotebookCellOutput output={output} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem key={cellId}>
|
||||
<NotebookCell cell={cell} language={language} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
42
packages/kbn-ipynb/components/notebook_cell.tsx
Normal file
42
packages/kbn-ipynb/components/notebook_cell.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiMarkdownFormat, EuiCodeBlock, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { NotebookCellType } from '../types';
|
||||
import { combineSource } from '../utils';
|
||||
|
||||
export const NotebookCell = ({ cell, language }: { cell: NotebookCellType; language: string }) => {
|
||||
if (!cell.cell_type) return null;
|
||||
const content = cell.source
|
||||
? combineSource(cell.source)
|
||||
: cell.input
|
||||
? combineSource(cell.input)
|
||||
: null;
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
switch (cell.cell_type) {
|
||||
case 'markdown':
|
||||
return <EuiMarkdownFormat>{content}</EuiMarkdownFormat>;
|
||||
case 'code':
|
||||
return (
|
||||
<EuiCodeBlock language={language} lineNumbers isCopyable>
|
||||
{content}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
case 'heading':
|
||||
return (
|
||||
<EuiTitle>
|
||||
<h2>{content}</h2>
|
||||
</EuiTitle>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
69
packages/kbn-ipynb/components/notebook_cell_output.tsx
Normal file
69
packages/kbn-ipynb/components/notebook_cell_output.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
|
||||
import { NotebookOutputType } from '../types';
|
||||
import { isDefined, isDefinedAndHasValue, combineSource } from '../utils';
|
||||
|
||||
export const NotebookCellOutput = ({ output }: { output: NotebookOutputType }) => {
|
||||
if (isDefined(output.text) && output.text.length > 0) {
|
||||
return <EuiCodeBlock>{combineSource(output.text)}</EuiCodeBlock>;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.png)) {
|
||||
return <OutputPng value={output.png} />;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.jpeg)) {
|
||||
return <OutputJpeg value={output.jpeg} />;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.gif)) {
|
||||
return <OutputGif value={output.gif} />;
|
||||
}
|
||||
if (!isDefined(output.data)) return null;
|
||||
if (isDefined(output.data['text/plain'])) {
|
||||
return <EuiCodeBlock>{combineSource(output.data['text/plain'])}</EuiCodeBlock>;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.data['image/png'])) {
|
||||
return <OutputPng value={output.data['image/png']} />;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.data['image/jpeg'])) {
|
||||
return <OutputJpeg value={output.data['image/jpeg']} />;
|
||||
}
|
||||
if (isDefinedAndHasValue(output.data['image/gif'])) {
|
||||
return <OutputGif value={output.data['image/gif']} />;
|
||||
}
|
||||
if (isDefined(output.data['text/html'])) {
|
||||
// We just present HTML instead of rendering it for safety
|
||||
return <EuiCodeBlock lineNumbers>{combineSource(output.data['text/html'])}</EuiCodeBlock>;
|
||||
}
|
||||
if (isDefined(output.data['application/javascript'])) {
|
||||
// We just present JS instead of rendering it for safety
|
||||
return (
|
||||
<EuiCodeBlock lineNumbers>
|
||||
{combineSource(output.data['application/javascript'])}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
if (isDefined(output.data['text/latex'])) {
|
||||
return <EuiCodeBlock lineNumbers>{combineSource(output.data['text/latex'])}</EuiCodeBlock>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const OutputImage = ({ value, type }: { value: string; type: 'png' | 'jpeg' | 'gif' }) => (
|
||||
<img
|
||||
src={`data:image/${type};base64,${value}`}
|
||||
alt={`output ${type} image`}
|
||||
style={{ maxHeight: '400px', maxWidth: '400px' }}
|
||||
/>
|
||||
);
|
||||
const OutputPng = ({ value }: { value: string }) => <OutputImage value={value} type="png" />;
|
||||
const OutputJpeg = ({ value }: { value: string }) => <OutputImage value={value} type="jpeg" />;
|
||||
const OutputGif = ({ value }: { value: string }) => <OutputImage value={value} type="gif" />;
|
10
packages/kbn-ipynb/index.tsx
Normal file
10
packages/kbn-ipynb/index.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './components';
|
13
packages/kbn-ipynb/jest.config.js
Normal file
13
packages/kbn-ipynb/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-ipynb'],
|
||||
};
|
5
packages/kbn-ipynb/kibana.jsonc
Normal file
5
packages/kbn-ipynb/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/ipynb",
|
||||
"owner": "@elastic/enterprise-search-frontend"
|
||||
}
|
6
packages/kbn-ipynb/package.json
Normal file
6
packages/kbn-ipynb/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/ipynb",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
19
packages/kbn-ipynb/tsconfig.json
Normal file
19
packages/kbn-ipynb/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
}
|
67
packages/kbn-ipynb/types.ts
Normal file
67
packages/kbn-ipynb/types.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export interface NotebookDefinition {
|
||||
cells: NotebookCellType[];
|
||||
metadata?: NotebookMetadataType;
|
||||
nbformat?: number;
|
||||
nbformat_minor?: number;
|
||||
}
|
||||
|
||||
export interface NotebookMetadataType {
|
||||
kernelspec?: {
|
||||
display_name?: string;
|
||||
language?: string;
|
||||
name?: string;
|
||||
};
|
||||
language_info?: {
|
||||
mimetype?: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotebookCellType {
|
||||
auto_number?: number;
|
||||
cell_type?: string;
|
||||
execution_count?: number | null;
|
||||
id?: string;
|
||||
input?: string[];
|
||||
metadata?: {
|
||||
id?: string;
|
||||
};
|
||||
outputs?: NotebookOutputType[];
|
||||
prompt_number?: number;
|
||||
source?: string[];
|
||||
}
|
||||
|
||||
export interface NotebookOutputType {
|
||||
name?: string;
|
||||
ename?: string;
|
||||
evalue?: string;
|
||||
traceback?: string[];
|
||||
data?: NotebookOutputData;
|
||||
output_type?: string;
|
||||
png?: string;
|
||||
jpeg?: string;
|
||||
gif?: string;
|
||||
svg?: string;
|
||||
text?: string[];
|
||||
execution_count?: number;
|
||||
}
|
||||
|
||||
export interface NotebookOutputData {
|
||||
'text/plain'?: string[];
|
||||
'text/html'?: string[];
|
||||
'text/latex'?: string[];
|
||||
'image/png'?: string;
|
||||
'image/jpeg'?: string;
|
||||
'image/gif'?: string;
|
||||
'image/svg+xml'?: string;
|
||||
'application/javascript'?: string[];
|
||||
}
|
33
packages/kbn-ipynb/utils.test.ts
Normal file
33
packages/kbn-ipynb/utils.test.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { isDefined, combineSource } from './utils';
|
||||
|
||||
describe('isDefined', () => {
|
||||
it('returns true if value is defined', () => {
|
||||
expect(isDefined({ foo: 'bar' })).toEqual(true);
|
||||
});
|
||||
it('returns false if value is null', () => {
|
||||
expect(isDefined(null)).toEqual(false);
|
||||
});
|
||||
it('returns false if value is undefined', () => {
|
||||
expect(isDefined(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combineSource', () => {
|
||||
it('returns value when given a string', () => {
|
||||
expect(combineSource('foobar')).toEqual('foobar');
|
||||
});
|
||||
it('returns combined string from array', () => {
|
||||
expect(combineSource(['foo', 'bar', 'baz'])).toEqual('foobarbaz');
|
||||
});
|
||||
it('returns combined string from array with separator', () => {
|
||||
expect(combineSource(['foo', 'bar', 'baz'], '\n')).toEqual('foo\nbar\nbaz');
|
||||
});
|
||||
});
|
20
packages/kbn-ipynb/utils.ts
Normal file
20
packages/kbn-ipynb/utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export function isDefined<T>(value: T | undefined | null): value is T {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
export function isDefinedAndHasValue(value: string | undefined): value is string {
|
||||
return isDefined(value) && value.length > 0;
|
||||
}
|
||||
|
||||
export const combineSource = (value: string | string[], separator: string = ''): string => {
|
||||
if (Array.isArray(value)) return value.join(separator);
|
||||
return value;
|
||||
};
|
|
@ -127,6 +127,7 @@ pageLoadAssetSize:
|
|||
screenshotMode: 17856
|
||||
screenshotting: 22870
|
||||
searchConnectors: 30000
|
||||
searchNotebooks: 18942
|
||||
searchPlayground: 19325
|
||||
searchprofiler: 67080
|
||||
security: 81771
|
||||
|
|
|
@ -64,11 +64,23 @@
|
|||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow-y: hidden; // Ensures the movement of buttons in :focus don't cause scrollbars
|
||||
overflow-x: auto;
|
||||
padding-right: $euiSizeS;
|
||||
|
||||
&--button {
|
||||
justify-content: flex-start;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
|
||||
.euiButtonEmpty__content {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&--altViewButton-container {
|
||||
margin-left: auto;
|
||||
// padding: $euiSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,8 @@ const loadDependencies = async (
|
|||
};
|
||||
};
|
||||
|
||||
interface ConsoleWrapperProps extends Omit<EmbeddableConsoleDependencies, 'setDispatch'> {
|
||||
interface ConsoleWrapperProps
|
||||
extends Omit<EmbeddableConsoleDependencies, 'setDispatch' | 'alternateView'> {
|
||||
onKeyDown: (this: Window, ev: WindowEventMap['keydown']) => any;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,11 +10,12 @@ import React, { useReducer, useEffect } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFocusTrap,
|
||||
EuiPortal,
|
||||
EuiScreenReaderOnly,
|
||||
EuiThemeProvider,
|
||||
EuiWindowEvent,
|
||||
keys,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -23,6 +24,7 @@ import { dynamic } from '@kbn/shared-ux-utility';
|
|||
import {
|
||||
EmbeddableConsoleProps,
|
||||
EmbeddableConsoleDependencies,
|
||||
EmbeddableConsoleView,
|
||||
} from '../../../types/embeddable_console';
|
||||
|
||||
import * as store from '../../stores/embeddable_console';
|
||||
|
@ -45,6 +47,7 @@ export const EmbeddableConsole = ({
|
|||
core,
|
||||
usageCollection,
|
||||
setDispatch,
|
||||
alternateView,
|
||||
}: EmbeddableConsoleProps & EmbeddableConsoleDependencies) => {
|
||||
const [consoleState, consoleDispatch] = useReducer(
|
||||
store.reducer,
|
||||
|
@ -57,22 +60,39 @@ export const EmbeddableConsole = ({
|
|||
return () => setDispatch(null);
|
||||
}, [setDispatch, consoleDispatch]);
|
||||
useEffect(() => {
|
||||
if (consoleState.isOpen && consoleState.loadFromContent) {
|
||||
if (consoleState.view === EmbeddableConsoleView.Console && consoleState.loadFromContent) {
|
||||
setLoadFromParameter(consoleState.loadFromContent);
|
||||
} else if (!consoleState.isOpen) {
|
||||
} else if (consoleState.view === EmbeddableConsoleView.Closed) {
|
||||
removeLoadFromParameter();
|
||||
}
|
||||
}, [consoleState.isOpen, consoleState.loadFromContent]);
|
||||
}, [consoleState.view, consoleState.loadFromContent]);
|
||||
useEffect(() => {
|
||||
document.body.classList.add(KBN_BODY_CONSOLE_CLASS);
|
||||
return () => document.body.classList.remove(KBN_BODY_CONSOLE_CLASS);
|
||||
}, []);
|
||||
|
||||
const isConsoleOpen = consoleState.isOpen;
|
||||
const isOpen = consoleState.view !== EmbeddableConsoleView.Closed;
|
||||
const showConsole =
|
||||
consoleState.view !== EmbeddableConsoleView.Closed &&
|
||||
(consoleState.view === EmbeddableConsoleView.Console || alternateView === undefined);
|
||||
const showAlternateView =
|
||||
consoleState.view === EmbeddableConsoleView.Alternate && alternateView !== undefined;
|
||||
const setIsConsoleOpen = (value: boolean) => {
|
||||
consoleDispatch(value ? { type: 'open' } : { type: 'close' });
|
||||
};
|
||||
const toggleConsole = () => setIsConsoleOpen(!isConsoleOpen);
|
||||
const toggleConsole = () => setIsConsoleOpen(!isOpen);
|
||||
const clickAlternateViewActivateButton: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
switch (consoleState.view) {
|
||||
case EmbeddableConsoleView.Console:
|
||||
case EmbeddableConsoleView.Closed:
|
||||
consoleDispatch({ type: 'open', payload: { alternateView: true } });
|
||||
break;
|
||||
case EmbeddableConsoleView.Alternate:
|
||||
consoleDispatch({ type: 'open', payload: { alternateView: false } });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: any) => {
|
||||
if (event.key === keys.ESCAPE) {
|
||||
|
@ -83,7 +103,7 @@ export const EmbeddableConsole = ({
|
|||
};
|
||||
|
||||
const classes = classNames('embeddableConsole', {
|
||||
'embeddableConsole-isOpen': isConsoleOpen,
|
||||
'embeddableConsole-isOpen': isOpen,
|
||||
'embeddableConsole--large': size === 'l',
|
||||
'embeddableConsole--medium': size === 'm',
|
||||
'embeddableConsole--small': size === 's',
|
||||
|
@ -96,7 +116,7 @@ export const EmbeddableConsole = ({
|
|||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFocusTrap onClickOutside={toggleConsole} disabled={!isConsoleOpen}>
|
||||
<EuiFocusTrap onClickOutside={toggleConsole} disabled={!isOpen}>
|
||||
<section
|
||||
aria-label={landmarkHeading}
|
||||
className={classes}
|
||||
|
@ -107,24 +127,35 @@ export const EmbeddableConsole = ({
|
|||
</EuiScreenReaderOnly>
|
||||
<EuiThemeProvider colorMode={'dark'} wrapperProps={{ cloneElement: true }}>
|
||||
<div className="embeddableConsole__controls">
|
||||
<EuiButton
|
||||
<EuiButtonEmpty
|
||||
color="text"
|
||||
iconType={isConsoleOpen ? 'arrowUp' : 'arrowDown'}
|
||||
iconType={isOpen ? 'arrowUp' : 'arrowDown'}
|
||||
onClick={toggleConsole}
|
||||
fullWidth
|
||||
contentProps={{
|
||||
className: 'embeddableConsole__controls--button',
|
||||
}}
|
||||
className="embeddableConsole__controls--button"
|
||||
data-test-subj="consoleEmbeddedControlBar"
|
||||
data-telemetry-id="console-embedded-controlbar-button"
|
||||
>
|
||||
{i18n.translate('console.embeddableConsole.title', {
|
||||
defaultMessage: 'Console',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiButtonEmpty>
|
||||
{alternateView && (
|
||||
<div className="embeddableConsole__controls--altViewButton-container">
|
||||
<alternateView.ActivationButton
|
||||
activeView={showAlternateView}
|
||||
onClick={clickAlternateViewActivateButton}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EuiThemeProvider>
|
||||
{isConsoleOpen ? <ConsoleWrapper {...{ core, usageCollection, onKeyDown }} /> : null}
|
||||
{showConsole ? <ConsoleWrapper {...{ core, usageCollection, onKeyDown }} /> : null}
|
||||
{showAlternateView ? (
|
||||
<div className="embeddableConsole__content" data-test-subj="consoleEmbeddedBody">
|
||||
<EuiWindowEvent event="keydown" handler={onKeyDown} />
|
||||
<alternateView.ViewContent />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
<EuiScreenReaderOnly>
|
||||
<p aria-live="assertive">
|
||||
|
|
|
@ -10,11 +10,15 @@ import { Reducer } from 'react';
|
|||
import { produce } from 'immer';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import { EmbeddedConsoleAction, EmbeddedConsoleStore } from '../../types/embeddable_console';
|
||||
import {
|
||||
EmbeddableConsoleView,
|
||||
EmbeddedConsoleAction,
|
||||
EmbeddedConsoleStore,
|
||||
} from '../../types/embeddable_console';
|
||||
|
||||
export const initialValue: EmbeddedConsoleStore = produce<EmbeddedConsoleStore>(
|
||||
{
|
||||
isOpen: false,
|
||||
view: EmbeddableConsoleView.Closed,
|
||||
},
|
||||
identity
|
||||
);
|
||||
|
@ -23,19 +27,22 @@ export const reducer: Reducer<EmbeddedConsoleStore, EmbeddedConsoleAction> = (st
|
|||
produce<EmbeddedConsoleStore>(state, (draft) => {
|
||||
switch (action.type) {
|
||||
case 'open':
|
||||
if (!state.isOpen) {
|
||||
draft.isOpen = true;
|
||||
const newView = action.payload?.alternateView
|
||||
? EmbeddableConsoleView.Alternate
|
||||
: EmbeddableConsoleView.Console;
|
||||
if (state.view !== newView) {
|
||||
draft.view = newView;
|
||||
draft.loadFromContent = action.payload?.content;
|
||||
return;
|
||||
return draft;
|
||||
}
|
||||
break;
|
||||
case 'close':
|
||||
if (state.isOpen) {
|
||||
draft.isOpen = false;
|
||||
if (state.view !== EmbeddableConsoleView.Closed) {
|
||||
draft.view = EmbeddableConsoleView.Closed;
|
||||
draft.loadFromContent = undefined;
|
||||
return;
|
||||
return draft;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return draft;
|
||||
return state;
|
||||
});
|
||||
|
|
|
@ -17,7 +17,9 @@ export type {
|
|||
ConsoleUILocatorParams,
|
||||
ConsolePluginSetup,
|
||||
ConsolePluginStart,
|
||||
EmbeddableConsoleProps as RemoteConsoleProps,
|
||||
EmbeddableConsoleProps,
|
||||
EmbeddedConsoleView,
|
||||
EmbeddedConsoleViewButtonProps,
|
||||
} from './types';
|
||||
|
||||
export { ConsoleUIPlugin as Plugin };
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ConsolePluginStart,
|
||||
ConsoleUILocatorParams,
|
||||
EmbeddableConsoleProps,
|
||||
EmbeddedConsoleView,
|
||||
} from './types';
|
||||
import { AutocompleteInfo, setAutocompleteInfo, EmbeddableConsoleInfo } from './services';
|
||||
|
||||
|
@ -132,12 +133,16 @@ export class ConsoleUIPlugin
|
|||
setDispatch: (d) => {
|
||||
this._embeddableConsole.setDispatch(d);
|
||||
},
|
||||
alternateView: this._embeddableConsole.alternateView,
|
||||
});
|
||||
};
|
||||
consoleStart.isEmbeddedConsoleAvailable = () =>
|
||||
this._embeddableConsole.isEmbeddedConsoleAvailable();
|
||||
consoleStart.openEmbeddedConsole = (content?: string) =>
|
||||
this._embeddableConsole.openEmbeddedConsole(content);
|
||||
consoleStart.registerEmbeddedConsoleAlternateView = (view: EmbeddedConsoleView | null) => {
|
||||
this._embeddableConsole.registerAlternateView(view);
|
||||
};
|
||||
}
|
||||
|
||||
return consoleStart;
|
||||
|
|
|
@ -7,10 +7,18 @@
|
|||
*/
|
||||
import type { Dispatch } from 'react';
|
||||
|
||||
import { EmbeddedConsoleAction as EmbeddableConsoleAction } from '../types/embeddable_console';
|
||||
import {
|
||||
EmbeddedConsoleAction as EmbeddableConsoleAction,
|
||||
EmbeddedConsoleView,
|
||||
} from '../types/embeddable_console';
|
||||
|
||||
export class EmbeddableConsoleInfo {
|
||||
private _dispatch: Dispatch<EmbeddableConsoleAction> | null = null;
|
||||
private _alternateView: EmbeddedConsoleView | undefined;
|
||||
|
||||
public get alternateView(): EmbeddedConsoleView | undefined {
|
||||
return this._alternateView;
|
||||
}
|
||||
|
||||
public setDispatch(d: Dispatch<EmbeddableConsoleAction> | null) {
|
||||
this._dispatch = d;
|
||||
|
@ -26,4 +34,8 @@ export class EmbeddableConsoleInfo {
|
|||
|
||||
this._dispatch({ type: 'open', payload: content ? { content } : undefined });
|
||||
}
|
||||
|
||||
public registerAlternateView(view: EmbeddedConsoleView | null) {
|
||||
this._alternateView = view ?? undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { ComponentType, MouseEventHandler } from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import type { Dispatch } from 'react';
|
||||
|
@ -23,13 +24,29 @@ export interface EmbeddableConsoleDependencies {
|
|||
core: CoreStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
setDispatch: (dispatch: Dispatch<EmbeddedConsoleAction> | null) => void;
|
||||
alternateView?: EmbeddedConsoleView;
|
||||
}
|
||||
|
||||
export type EmbeddedConsoleAction =
|
||||
| { type: 'open'; payload?: { content?: string } }
|
||||
| { type: 'open'; payload?: { content?: string; alternateView?: boolean } }
|
||||
| { type: 'close' };
|
||||
|
||||
export enum EmbeddableConsoleView {
|
||||
Closed,
|
||||
Console,
|
||||
Alternate,
|
||||
}
|
||||
|
||||
export interface EmbeddedConsoleStore {
|
||||
isOpen: boolean;
|
||||
view: EmbeddableConsoleView;
|
||||
loadFromContent?: string;
|
||||
}
|
||||
|
||||
export interface EmbeddedConsoleViewButtonProps {
|
||||
activeView: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
export interface EmbeddedConsoleView {
|
||||
ActivationButton: ComponentType<EmbeddedConsoleViewButtonProps>;
|
||||
ViewContent: ComponentType<{}>;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collectio
|
|||
import { SharePluginSetup, SharePluginStart, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
|
||||
import { ConsoleUILocatorParams } from './locator';
|
||||
import { EmbeddableConsoleProps } from './embeddable_console';
|
||||
import { EmbeddableConsoleProps, EmbeddedConsoleView } from './embeddable_console';
|
||||
|
||||
export interface AppSetupUIPluginDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
|
@ -56,4 +56,10 @@ export interface ConsolePluginStart {
|
|||
* EmbeddableConsole is a functional component used to render a portable version of the dev tools console on any page in Kibana
|
||||
*/
|
||||
EmbeddableConsole?: FC<EmbeddableConsoleProps>;
|
||||
/**
|
||||
* Register an alternate view for the Embedded Console
|
||||
*
|
||||
* When registering an alternate view ensure that the content component you register is lazy loaded.
|
||||
*/
|
||||
registerEmbeddedConsoleAlternateView?: (view: EmbeddedConsoleView | null) => void;
|
||||
}
|
||||
|
|
|
@ -974,6 +974,8 @@
|
|||
"@kbn/interpreter/*": ["packages/kbn-interpreter/*"],
|
||||
"@kbn/io-ts-utils": ["packages/kbn-io-ts-utils"],
|
||||
"@kbn/io-ts-utils/*": ["packages/kbn-io-ts-utils/*"],
|
||||
"@kbn/ipynb": ["packages/kbn-ipynb"],
|
||||
"@kbn/ipynb/*": ["packages/kbn-ipynb/*"],
|
||||
"@kbn/jest-serializers": ["packages/kbn-jest-serializers"],
|
||||
"@kbn/jest-serializers/*": ["packages/kbn-jest-serializers/*"],
|
||||
"@kbn/journeys": ["packages/kbn-journeys"],
|
||||
|
|
36
x-pack/plugins/search_notebooks/common/constants.ts
Normal file
36
x-pack/plugins/search_notebooks/common/constants.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Notebook } from './types';
|
||||
|
||||
export const INTRODUCTION_NOTEBOOK: Notebook = {
|
||||
id: 'introduction',
|
||||
title: i18n.translate('xpack.searchNotebooks.introductionNotebook.title', {
|
||||
defaultMessage: 'What are Jupyter Notebooks?',
|
||||
}),
|
||||
description: i18n.translate('xpack.searchNotebooks.introductionNotebook.description', {
|
||||
defaultMessage:
|
||||
'Jupyter Notebooks are an open-source document format for sharing interactive code embedded in narrative text.',
|
||||
}),
|
||||
notebook: {
|
||||
cells: [
|
||||
{
|
||||
cell_type: 'markdown',
|
||||
source: [
|
||||
'# What are Jupyter Notebooks\n',
|
||||
'\n',
|
||||
'Jupyter Notebooks combine executable code and rich Markdown documentation in a single interactive document. Easy to run, edit and share, they enable collaboration in fields like data science, scientific computing, and machine learning.',
|
||||
],
|
||||
},
|
||||
{
|
||||
cell_type: 'code',
|
||||
source: ['print("Hello world!!!")'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
25
x-pack/plugins/search_notebooks/common/types.ts
Normal file
25
x-pack/plugins/search_notebooks/common/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NotebookDefinition } from '@kbn/ipynb';
|
||||
|
||||
export interface NotebookInformation {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface NotebookCatalog {
|
||||
notebooks: NotebookInformation[];
|
||||
}
|
||||
|
||||
export interface Notebook extends NotebookInformation {
|
||||
link?: {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
notebook: NotebookDefinition;
|
||||
}
|
|
@ -6,14 +6,16 @@
|
|||
"plugin": {
|
||||
"id": "searchNotebooks",
|
||||
"server": true,
|
||||
"browser": false,
|
||||
"browser": true,
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"search",
|
||||
"notebooks"
|
||||
],
|
||||
"requiredPlugins": [],
|
||||
"requiredPlugins": [
|
||||
"console"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": []
|
||||
"requiredBundles": ["kibanaReact"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
export const LoadingPanel = () => (
|
||||
<EuiPanel color="subdued">
|
||||
<EuiLoadingSpinner />
|
||||
</EuiPanel>
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EmbeddedConsoleViewButtonProps } from '@kbn/console-plugin/public';
|
||||
|
||||
export const SearchNotebooksButton = ({ activeView, onClick }: EmbeddedConsoleViewButtonProps) => {
|
||||
if (activeView) {
|
||||
return (
|
||||
<EuiButton
|
||||
color="success"
|
||||
fill
|
||||
onClick={onClick}
|
||||
size="s"
|
||||
iconType="documentation"
|
||||
iconSide="left"
|
||||
data-test-subj="consoleEmbeddedNotebooksButton"
|
||||
data-telemetry-id="console-embedded-notebooks-button"
|
||||
>
|
||||
{i18n.translate('xpack.searchNotebooks.notebooksButton.title', {
|
||||
defaultMessage: 'Notebooks',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="success"
|
||||
onClick={onClick}
|
||||
size="s"
|
||||
iconType="documentation"
|
||||
iconSide="left"
|
||||
data-test-subj="consoleEmbeddedNotebooksButton"
|
||||
data-telemetry-id="console-embedded-notebooks-button"
|
||||
>
|
||||
{i18n.translate('xpack.searchNotebooks.notebooksButton.title', {
|
||||
defaultMessage: 'Notebooks',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {} from '@elastic/eui';
|
||||
|
||||
import { NotebookInformation } from '../../common/types';
|
||||
import { LoadingPanel } from './loading_panel';
|
||||
import { SelectionPanel } from './selection_panel';
|
||||
|
||||
export interface NotebooksListProps {
|
||||
notebooks: NotebookInformation[] | null;
|
||||
onNotebookSelect: (id: string) => void;
|
||||
selectedNotebookId: string;
|
||||
}
|
||||
export const NotebooksList = ({
|
||||
notebooks,
|
||||
onNotebookSelect,
|
||||
selectedNotebookId,
|
||||
}: NotebooksListProps) => {
|
||||
if (notebooks === null) {
|
||||
// Loading Notebooks
|
||||
return <LoadingPanel />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{notebooks.map((notebook) => (
|
||||
<SelectionPanel
|
||||
key={notebook.id}
|
||||
id={notebook.id}
|
||||
title={notebook.title}
|
||||
description={notebook.description}
|
||||
onClick={onNotebookSelect}
|
||||
isSelected={selectedNotebookId === notebook.id}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { SearchNotebooks } from './search_notebooks';
|
||||
|
||||
export interface SearchNotebooksViewProps {
|
||||
core: CoreStart;
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
export const SearchNotebooksView = ({ core, queryClient }: SearchNotebooksViewProps) => (
|
||||
<KibanaThemeProvider theme={core.theme}>
|
||||
<KibanaContextProvider services={{ ...core }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SearchNotebooks />
|
||||
</QueryClientProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiButton, EuiFlexGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SearchLabsButtonPanel = () => {
|
||||
return (
|
||||
<EuiPanel hasShadow={false}>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiButton
|
||||
href="https://github.com/elastic/elasticsearch-labs"
|
||||
target="_blank"
|
||||
iconSide="right"
|
||||
iconType="popout"
|
||||
data-test-subj="console-notebooks-search-labs-btn"
|
||||
data-telemetry-id="console-notebooks-search-labs-btn"
|
||||
>
|
||||
{i18n.translate('xpack.searchNotebooks.searchLabsLink', {
|
||||
defaultMessage: 'See more at Elastic Search Labs',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiEmptyPrompt, EuiCodeBlock } from '@elastic/eui';
|
||||
import { NotebookRenderer } from '@kbn/ipynb';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useNotebook } from '../hooks/use_notebook';
|
||||
import { LoadingPanel } from './loading_panel';
|
||||
|
||||
export interface SearchNotebookProps {
|
||||
notebookId: string;
|
||||
}
|
||||
export const SearchNotebook = ({ notebookId }: SearchNotebookProps) => {
|
||||
const { data, isLoading, error } = useNotebook(notebookId);
|
||||
if (isLoading) {
|
||||
return <LoadingPanel />;
|
||||
}
|
||||
if (!data || error) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="warning"
|
||||
iconColor="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.searchNotebooks.notebook.fetchError.title"
|
||||
defaultMessage="Error loading notebook"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="l"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.searchNotebooks.notebook.fetchError.body"
|
||||
defaultMessage="We can't fetch the notebook from Kibana due to the following error:"
|
||||
/>
|
||||
</p>
|
||||
<EuiCodeBlock css={{ textAlign: 'left' }}>{JSON.stringify(error)}</EuiCodeBlock>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiPanel
|
||||
paddingSize="xl"
|
||||
hasShadow={false}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
data-test-subj={`console-embedded-notebook-view-panel-${notebookId}`}
|
||||
>
|
||||
<NotebookRenderer notebook={data.notebook} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiResizableContainer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { INTRODUCTION_NOTEBOOK } from '../../common/constants';
|
||||
import { useNotebooksCatalog } from '../hooks/use_notebook_catalog';
|
||||
import { NotebooksList } from './notebooks_list';
|
||||
import { SelectionPanel } from './selection_panel';
|
||||
import { TitlePanel } from './title_panel';
|
||||
import { SearchNotebook } from './search_notebook';
|
||||
import { SearchLabsButtonPanel } from './search_labs_button_panel';
|
||||
|
||||
const LIST_PANEL_ID = 'notebooksList';
|
||||
const OUTPUT_PANEL_ID = 'notebooksOutput';
|
||||
const defaultSizes: Record<string, number> = {
|
||||
[LIST_PANEL_ID]: 25,
|
||||
[OUTPUT_PANEL_ID]: 75,
|
||||
};
|
||||
|
||||
export const SearchNotebooks = () => {
|
||||
const [sizes, setSizes] = useState(defaultSizes);
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string>('introduction');
|
||||
const { data } = useNotebooksCatalog();
|
||||
const onPanelWidthChange = useCallback((newSizes: Record<string, number>) => {
|
||||
setSizes((prevSizes: Record<string, number>) => ({
|
||||
...prevSizes,
|
||||
...newSizes,
|
||||
}));
|
||||
}, []);
|
||||
const notebooks = useMemo(() => {
|
||||
if (data) return data.notebooks;
|
||||
return null;
|
||||
}, [data]);
|
||||
const onNotebookSelectionClick = useCallback((id: string) => {
|
||||
setSelectedNotebookId(id);
|
||||
}, []);
|
||||
return (
|
||||
<EuiResizableContainer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onPanelWidthChange={onPanelWidthChange}
|
||||
data-test-subj="consoleEmbeddedNotebooksContainer"
|
||||
>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
id={LIST_PANEL_ID}
|
||||
size={sizes[LIST_PANEL_ID]}
|
||||
minSize="10%"
|
||||
tabIndex={0}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<TitlePanel>
|
||||
{i18n.translate('xpack.searchNotebooks.notebooksList.introduction.title', {
|
||||
defaultMessage: 'Introduction',
|
||||
})}
|
||||
</TitlePanel>
|
||||
<SelectionPanel
|
||||
id={INTRODUCTION_NOTEBOOK.id}
|
||||
title={INTRODUCTION_NOTEBOOK.title}
|
||||
description={INTRODUCTION_NOTEBOOK.description}
|
||||
onClick={onNotebookSelectionClick}
|
||||
isSelected={selectedNotebookId === INTRODUCTION_NOTEBOOK.id}
|
||||
/>
|
||||
<TitlePanel>
|
||||
{i18n.translate('xpack.searchNotebooks.notebooksList.availableNotebooks.title', {
|
||||
defaultMessage: 'Available Notebooks',
|
||||
})}
|
||||
</TitlePanel>
|
||||
<NotebooksList
|
||||
notebooks={notebooks}
|
||||
selectedNotebookId={selectedNotebookId}
|
||||
onNotebookSelect={onNotebookSelectionClick}
|
||||
/>
|
||||
<SearchLabsButtonPanel />
|
||||
</EuiFlexGroup>
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton />
|
||||
|
||||
<EuiResizablePanel
|
||||
id={OUTPUT_PANEL_ID}
|
||||
size={sizes[OUTPUT_PANEL_ID]}
|
||||
minSize="200px"
|
||||
tabIndex={0}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TitlePanel>
|
||||
{i18n.translate('xpack.searchNotebooks.notebooksList.activeNotebook.title', {
|
||||
defaultMessage: 'Active notebook',
|
||||
})}
|
||||
</TitlePanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SearchNotebook notebookId={selectedNotebookId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export interface SelectionPanelProps {
|
||||
description: string;
|
||||
id: string;
|
||||
isSelected: boolean;
|
||||
onClick: (id: string) => void;
|
||||
title: string;
|
||||
}
|
||||
export const SelectionPanel = ({
|
||||
description,
|
||||
id,
|
||||
isSelected,
|
||||
title,
|
||||
onClick,
|
||||
}: SelectionPanelProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<>
|
||||
<EuiPanel
|
||||
data-test-subj={`console-embedded-notebook-select-btn-${id}`}
|
||||
data-telemdata-telemetry-id={`console-embedded-notebook-select-btn-${id}`}
|
||||
onClick={() => onClick(id)}
|
||||
color={isSelected ? 'primary' : 'subdued'}
|
||||
hasBorder
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<EuiTextColor color={euiTheme.colors.primaryText}>{title}</EuiTextColor>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="s" color={isSelected ? euiTheme.colors.primaryText : 'subdued'}>
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui';
|
||||
|
||||
export const TitlePanel: React.FC = ({ children }) => (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} paddingSize="s">
|
||||
<EuiText size="s" color="subdued">
|
||||
{children}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
);
|
28
x-pack/plugins/search_notebooks/public/console_view.tsx
Normal file
28
x-pack/plugins/search_notebooks/public/console_view.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { EmbeddedConsoleView } from '@kbn/console-plugin/public';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { SearchNotebooksButton } from './components/notebooks_button';
|
||||
|
||||
const SearchNotebooksView = dynamic(async () => ({
|
||||
default: (await import('./components/notebooks_view')).SearchNotebooksView,
|
||||
}));
|
||||
|
||||
export const notebooksConsoleView = (
|
||||
core: CoreStart,
|
||||
queryClient: QueryClient
|
||||
): EmbeddedConsoleView => {
|
||||
return {
|
||||
ActivationButton: SearchNotebooksButton,
|
||||
ViewContent: () => <SearchNotebooksView core={core} queryClient={queryClient} />,
|
||||
};
|
||||
};
|
18
x-pack/plugins/search_notebooks/public/hooks/use_kibana.ts
Normal file
18
x-pack/plugins/search_notebooks/public/hooks/use_kibana.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ConsolePluginStart } from '@kbn/console-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { useKibana as useKibanaBase } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
export interface SearchNotebooksContext {
|
||||
console: ConsolePluginStart;
|
||||
}
|
||||
|
||||
type ServerlessSearchKibanaContext = CoreStart & SearchNotebooksContext;
|
||||
|
||||
export const useKibanaServices = () => useKibanaBase<ServerlessSearchKibanaContext>().services;
|
18
x-pack/plugins/search_notebooks/public/hooks/use_notebook.ts
Normal file
18
x-pack/plugins/search_notebooks/public/hooks/use_notebook.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Notebook } from '../../common/types';
|
||||
import { useKibanaServices } from './use_kibana';
|
||||
|
||||
export const useNotebook = (id: string) => {
|
||||
const { http } = useKibanaServices();
|
||||
return useQuery({
|
||||
queryKey: ['fetchSearchNotebook', id],
|
||||
queryFn: () => http.get<Notebook>(`/internal/search_notebooks/notebooks/${id}`),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { NotebookCatalog } from '../../common/types';
|
||||
import { useKibanaServices } from './use_kibana';
|
||||
|
||||
export const useNotebooksCatalog = () => {
|
||||
const { http } = useKibanaServices();
|
||||
return useQuery({
|
||||
queryKey: ['fetchNotebooksCatalog'],
|
||||
queryFn: () => http.get<NotebookCatalog>('/internal/search_notebooks/notebooks'),
|
||||
});
|
||||
};
|
14
x-pack/plugins/search_notebooks/public/index.ts
Normal file
14
x-pack/plugins/search_notebooks/public/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SearchNotebooksPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new SearchNotebooksPlugin();
|
||||
}
|
||||
|
||||
export type { SearchNotebooksPluginSetup, SearchNotebooksPluginStart } from './types';
|
64
x-pack/plugins/search_notebooks/public/plugin.ts
Normal file
64
x-pack/plugins/search_notebooks/public/plugin.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup, Plugin, CoreStart } from '@kbn/core/public';
|
||||
import { QueryClient, MutationCache, QueryCache } from '@tanstack/react-query';
|
||||
|
||||
import { notebooksConsoleView } from './console_view';
|
||||
import {
|
||||
SearchNotebooksPluginSetup,
|
||||
SearchNotebooksPluginStart,
|
||||
SearchNotebooksPluginStartDependencies,
|
||||
} from './types';
|
||||
import { getErrorCode, getErrorMessage, isKibanaServerError } from './utils/get_error_message';
|
||||
|
||||
export class SearchNotebooksPlugin
|
||||
implements Plugin<SearchNotebooksPluginSetup, SearchNotebooksPluginStart>
|
||||
{
|
||||
private queryClient: QueryClient | undefined;
|
||||
public setup(core: CoreSetup): SearchNotebooksPluginSetup {
|
||||
this.queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
core.notifications.toasts.addError(error as Error, {
|
||||
title: (error as Error).name,
|
||||
toastMessage: getErrorMessage(error),
|
||||
toastLifeTimeMs: 1000,
|
||||
});
|
||||
},
|
||||
}),
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => {
|
||||
// 404s are often functionally okay and shouldn't show toasts by default
|
||||
if (getErrorCode(error) === 404) {
|
||||
return;
|
||||
}
|
||||
if (isKibanaServerError(error) && !error.skipToast) {
|
||||
core.notifications.toasts.addError(error, {
|
||||
title: error.name,
|
||||
toastMessage: getErrorMessage(error),
|
||||
toastLifeTimeMs: 1000,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
return {};
|
||||
}
|
||||
public start(
|
||||
core: CoreStart,
|
||||
deps: SearchNotebooksPluginStartDependencies
|
||||
): SearchNotebooksPluginStart {
|
||||
if (deps.console?.registerEmbeddedConsoleAlternateView) {
|
||||
deps.console.registerEmbeddedConsoleAlternateView(
|
||||
notebooksConsoleView(core, this.queryClient!)
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
public stop() {}
|
||||
}
|
17
x-pack/plugins/search_notebooks/public/types.ts
Normal file
17
x-pack/plugins/search_notebooks/public/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ConsolePluginStart } from '@kbn/console-plugin/public';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SearchNotebooksPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SearchNotebooksPluginStart {}
|
||||
|
||||
export interface SearchNotebooksPluginStartDependencies {
|
||||
console: ConsolePluginStart;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (isKibanaServerError(error)) {
|
||||
return error.body.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && (error as { name: string }).name) {
|
||||
return (error as { name: string }).name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getErrorCode(error: unknown): number | undefined {
|
||||
if (isKibanaServerError(error)) {
|
||||
return error.body.statusCode;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isKibanaServerError(
|
||||
input: unknown
|
||||
): input is Error & { body: KibanaServerError; name: string; skipToast?: boolean } {
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
(input as { body: KibanaServerError }).body &&
|
||||
typeof (input as { body: KibanaServerError }).body.message === 'string'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
|
@ -11,7 +11,7 @@ import { PluginConfigDescriptor } from '@kbn/core/server';
|
|||
export * from './types';
|
||||
|
||||
const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
type SearchNotebooksSchema = TypeOf<typeof configSchema>;
|
||||
|
|
|
@ -9,8 +9,9 @@ import fs from 'fs/promises';
|
|||
import path from 'path';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { NotebookDefinition } from '@kbn/ipynb';
|
||||
|
||||
import { NotebookCatalog, NotebookInformation, NotebookDefinition } from '../types';
|
||||
import { NotebookCatalog, NotebookInformation } from '../../common/types';
|
||||
|
||||
const NOTEBOOKS_DATA_DIR = '../data';
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { NotebookDefinition } from '@kbn/ipynb';
|
||||
|
||||
import { INTRODUCTION_NOTEBOOK } from '../../common/constants';
|
||||
import { DEFAULT_NOTEBOOKS, NOTEBOOKS_MAP, getNotebook } from '../lib/notebook_catalog';
|
||||
import { NotebookDefinition } from '../types';
|
||||
|
||||
export function defineRoutes(router: IRouter, logger: Logger) {
|
||||
router.get(
|
||||
|
@ -35,9 +36,16 @@ export function defineRoutes(router: IRouter, logger: Logger) {
|
|||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
async (_, request, response) => {
|
||||
const notebookId = request.params.notebookId;
|
||||
|
||||
if (notebookId === INTRODUCTION_NOTEBOOK.id) {
|
||||
return response.ok({
|
||||
body: INTRODUCTION_NOTEBOOK,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
if (!NOTEBOOKS_MAP.hasOwnProperty(notebookId)) {
|
||||
logger.warn(`Unknown search notebook requested ${notebookId}`);
|
||||
return response.notFound();
|
||||
|
|
|
@ -9,81 +9,3 @@
|
|||
export interface SearchNotebooksPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface SearchNotebooksPluginStart {}
|
||||
|
||||
export interface NotebookInformation {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
export interface NotebookCatalog {
|
||||
notebooks: NotebookInformation[];
|
||||
}
|
||||
|
||||
export interface Notebook extends NotebookInformation {
|
||||
link?: {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
notebook: NotebookDefinition;
|
||||
}
|
||||
|
||||
export interface NotebookDefinition {
|
||||
cells: NotebookCellType[];
|
||||
metadata?: NotebookMetadataType;
|
||||
nbformat?: number;
|
||||
nbformat_minor?: number;
|
||||
}
|
||||
|
||||
export interface NotebookMetadataType {
|
||||
kernelspec?: {
|
||||
display_name?: string;
|
||||
language?: string;
|
||||
name?: string;
|
||||
};
|
||||
language_info?: {
|
||||
mimetype?: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotebookCellType {
|
||||
auto_number?: number;
|
||||
cell_type?: string;
|
||||
execution_count?: number | null;
|
||||
id?: string;
|
||||
inputs?: string[];
|
||||
metadata?: {
|
||||
id?: string;
|
||||
};
|
||||
outputs?: NotebookOutputType[];
|
||||
prompt_number?: number;
|
||||
source?: string[];
|
||||
}
|
||||
|
||||
export interface NotebookOutputType {
|
||||
name?: string;
|
||||
ename?: string;
|
||||
evalue?: string;
|
||||
traceback?: string[];
|
||||
data?: {
|
||||
'text/plain'?: string[];
|
||||
'text/html'?: string[];
|
||||
'text/latex'?: string[];
|
||||
'image/png'?: string;
|
||||
'image/jpeg'?: string;
|
||||
'image/gif'?: string;
|
||||
'image/svg+xml'?: string;
|
||||
'application/javascript'?: string[];
|
||||
};
|
||||
output_type?: string;
|
||||
png?: string;
|
||||
jpeg?: string;
|
||||
gif?: string;
|
||||
svg?: string;
|
||||
text?: string[];
|
||||
execution_count?: number;
|
||||
metadata?: {
|
||||
scrolled?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,5 +19,12 @@
|
|||
"@kbn/core",
|
||||
"@kbn/i18n",
|
||||
"@kbn/logging",
|
||||
"@kbn/console-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/ipynb",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -310,6 +310,27 @@ export function SvlCommonNavigationProvider(ctx: FtrProviderContext) {
|
|||
async clickEmbeddedConsoleControlBar() {
|
||||
await testSubjects.click('consoleEmbeddedControlBar');
|
||||
},
|
||||
async expectEmbeddedConsoleNotebooksButtonExists() {
|
||||
await testSubjects.existOrFail('consoleEmbeddedNotebooksButton');
|
||||
},
|
||||
async clickEmbeddedConsoleNotebooksButton() {
|
||||
await testSubjects.click('consoleEmbeddedNotebooksButton');
|
||||
},
|
||||
async expectEmbeddedConsoleNotebooksToBeOpen() {
|
||||
await testSubjects.existOrFail('consoleEmbeddedNotebooksContainer');
|
||||
},
|
||||
async expectEmbeddedConsoleNotebooksToBeClosed() {
|
||||
await testSubjects.missingOrFail('consoleEmbeddedNotebooksContainer');
|
||||
},
|
||||
async expectEmbeddedConsoleNotebookListItemToBeAvailable(id: string) {
|
||||
await testSubjects.existOrFail(`console-embedded-notebook-select-btn-${id}`);
|
||||
},
|
||||
async clickEmbeddedConsoleNotebook(id: string) {
|
||||
await testSubjects.click(`console-embedded-notebook-select-btn-${id}`);
|
||||
},
|
||||
async expectEmbeddedConsoleNotebookToBeAvailable(id: string) {
|
||||
await testSubjects.click(`console-embedded-notebook-select-btn-${id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const pageObjects = getPageObjects(['svlCommonPage', 'svlCommonNavigation']);
|
||||
|
||||
describe('Console Notebooks', function () {
|
||||
before(async () => {
|
||||
await pageObjects.svlCommonPage.login();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await pageObjects.svlCommonPage.forceLogout();
|
||||
});
|
||||
|
||||
it('has notebooks view available', async () => {
|
||||
// Expect Console Bar & Notebooks button to exist
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleControlBarExists();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksButtonExists();
|
||||
|
||||
// Click the Notebooks button to open console to See Notebooks
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleNotebooksButton();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeOpen();
|
||||
|
||||
// Click the Notebooks button again to switch to the dev console
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleNotebooksButton();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleToBeOpen();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeClosed();
|
||||
|
||||
// Clicking control bar should close the console
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleControlBar();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeClosed();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleToBeClosed();
|
||||
|
||||
// Re-open console and then open Notebooks
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleControlBar();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleToBeOpen();
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleNotebooksButton();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeOpen();
|
||||
|
||||
// Close the console
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleControlBar();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeClosed();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleToBeClosed();
|
||||
});
|
||||
|
||||
it('can open notebooks', async () => {
|
||||
// Click the Notebooks button to open console to See Notebooks
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleNotebooksButton();
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebooksToBeOpen();
|
||||
|
||||
const defaultNotebooks = [
|
||||
'00_quick_start',
|
||||
'01_keyword_querying_filtering',
|
||||
'02_hybrid_search',
|
||||
'03_elser',
|
||||
'04_multilingual',
|
||||
];
|
||||
for (const notebookId of defaultNotebooks) {
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebookListItemToBeAvailable(
|
||||
notebookId
|
||||
);
|
||||
await pageObjects.svlCommonNavigation.devConsole.clickEmbeddedConsoleNotebook(notebookId);
|
||||
await pageObjects.svlCommonNavigation.devConsole.expectEmbeddedConsoleNotebookToBeAvailable(
|
||||
notebookId
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -18,6 +18,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./dashboards/import_dashboard'));
|
||||
loadTestFile(require.resolve('./advanced_settings'));
|
||||
loadTestFile(require.resolve('./rules/rule_details'));
|
||||
loadTestFile(require.resolve('./console_notebooks'));
|
||||
|
||||
loadTestFile(require.resolve('./ml'));
|
||||
});
|
||||
|
|
|
@ -5000,6 +5000,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/ipynb@link:packages/kbn-ipynb":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/jest-serializers@link:packages/kbn-jest-serializers":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue