mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Search][Notebooks] allow pre-selecting a specific notebook (#189825)
## Summary Updated the search notebooks to allow loading the selected notebooks from a query parameter. exposes a function on the search notebooks start contract to select notebook. Tested to ensure this allows the user to change the selected notebooks once the notebook view is open. caveats: - The way this currently works is you have to set the selected notebook before opening the notebooks view. - If you set the query parameter when the notebook view is open the selection DOES NOT update, I tried to implement this but ran into issues with not being able to change the selection or unrelated re-renders reverting the selection back to the query parameter value :/ ### Checklist - [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 Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
507f1d2863
commit
40d1a91bac
7 changed files with 295 additions and 6 deletions
|
@ -65,3 +65,5 @@ export const INTRODUCTION_NOTEBOOK: Notebook = {
|
|||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_NOTEBOOK_ID = 'introduction';
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {} from '@elastic/eui';
|
||||
|
||||
import { NotebookInformation } from '../../common/types';
|
||||
import { LoadingPanel } from './loading_panel';
|
||||
|
|
|
@ -4,17 +4,19 @@
|
|||
* 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { EuiResizableContainer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { INTRODUCTION_NOTEBOOK } from '../../common/constants';
|
||||
import { INTRODUCTION_NOTEBOOK, DEFAULT_NOTEBOOK_ID } 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';
|
||||
import { readNotebookParameter } from '../utils/notebook_query_param';
|
||||
|
||||
const LIST_PANEL_ID = 'notebooksList';
|
||||
const OUTPUT_PANEL_ID = 'notebooksOutput';
|
||||
|
@ -25,7 +27,9 @@ const defaultSizes: Record<string, number> = {
|
|||
|
||||
export const SearchNotebooks = () => {
|
||||
const [sizes, setSizes] = useState(defaultSizes);
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string>('introduction');
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string>(
|
||||
readNotebookParameter() ?? DEFAULT_NOTEBOOK_ID
|
||||
);
|
||||
const { data } = useNotebooksCatalog();
|
||||
const onPanelWidthChange = useCallback((newSizes: Record<string, number>) => {
|
||||
setSizes((prevSizes: Record<string, number>) => ({
|
||||
|
@ -33,6 +37,16 @@ export const SearchNotebooks = () => {
|
|||
...newSizes,
|
||||
}));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const selectedNotebookFound =
|
||||
data.notebooks.find((nb) => nb.id === selectedNotebookId) !== undefined;
|
||||
if (!selectedNotebookFound) {
|
||||
// If the currently selected notebook is not in the list of notebooks revert
|
||||
// to the default notebook selection.
|
||||
setSelectedNotebookId(DEFAULT_NOTEBOOK_ID);
|
||||
}
|
||||
}, [data, selectedNotebookId]);
|
||||
const notebooks = useMemo(() => {
|
||||
if (data) return data.notebooks;
|
||||
return null;
|
||||
|
@ -40,6 +54,7 @@ export const SearchNotebooks = () => {
|
|||
const onNotebookSelectionClick = useCallback((id: string) => {
|
||||
setSelectedNotebookId(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiResizableContainer
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from './types';
|
||||
import { getErrorCode, getErrorMessage, isKibanaServerError } from './utils/get_error_message';
|
||||
import { createUsageTracker } from './utils/usage_tracker';
|
||||
import { removeNotebookParameter, setNotebookParameter } from './utils/notebook_query_param';
|
||||
|
||||
export class SearchNotebooksPlugin
|
||||
implements Plugin<SearchNotebooksPluginSetup, SearchNotebooksPluginStart>
|
||||
|
@ -66,7 +67,7 @@ export class SearchNotebooksPlugin
|
|||
core,
|
||||
this.queryClient!,
|
||||
this.usageTracker,
|
||||
this.clearNotebookList.bind(this),
|
||||
this.clearNotebooksState.bind(this),
|
||||
this.getNotebookList.bind(this)
|
||||
)
|
||||
);
|
||||
|
@ -75,12 +76,16 @@ export class SearchNotebooksPlugin
|
|||
setNotebookList: (value: NotebookListValue) => {
|
||||
this.setNotebookList(value);
|
||||
},
|
||||
setSelectedNotebook: (value: string) => {
|
||||
setNotebookParameter(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
public stop() {}
|
||||
|
||||
private clearNotebookList() {
|
||||
private clearNotebooksState() {
|
||||
this.setNotebookList(null);
|
||||
removeNotebookParameter();
|
||||
}
|
||||
|
||||
private setNotebookList(value: NotebookListValue) {
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface SearchNotebooksPluginSetup {}
|
|||
|
||||
export interface SearchNotebooksPluginStart {
|
||||
setNotebookList: (value: NotebookListValue) => void;
|
||||
setSelectedNotebook: (notebookId: string) => void;
|
||||
}
|
||||
|
||||
export interface SearchNotebooksPluginStartDependencies {
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { setNotebookParameter, removeNotebookParameter } from './notebook_query_param';
|
||||
|
||||
const baseMockWindow = () => {
|
||||
return {
|
||||
history: {
|
||||
pushState: jest.fn(),
|
||||
},
|
||||
location: {
|
||||
host: 'my-kibana.elastic.co',
|
||||
pathname: '',
|
||||
protocol: 'https:',
|
||||
search: '',
|
||||
hash: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
let windowSpy: jest.SpyInstance;
|
||||
let mockWindow = baseMockWindow();
|
||||
|
||||
describe('notebook query parameter utility', () => {
|
||||
beforeEach(() => {
|
||||
mockWindow = baseMockWindow();
|
||||
windowSpy = jest.spyOn(globalThis, 'window', 'get');
|
||||
windowSpy.mockImplementation(() => mockWindow);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
windowSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('setNotebookParameter', () => {
|
||||
it('adds notebookId query param', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
};
|
||||
const notebook = '00_quick_start';
|
||||
const expectedUrl =
|
||||
'https://my-kibana.elastic.co/foo/app/elasticsearch?notebookId=AzD6EcFcEsGMGtQGcAuBDATioA';
|
||||
|
||||
setNotebookParameter(notebook);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('can replace an existing value', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
search: '?notebookId=AwRg+g1gpgng7gewE4BMwEcCuUkwJYB2A5mAGZ4A2ALjoUUA',
|
||||
};
|
||||
const notebook = '00_quick_start';
|
||||
const expectedUrl =
|
||||
'https://my-kibana.elastic.co/foo/app/elasticsearch?notebookId=AzD6EcFcEsGMGtQGcAuBDATioA';
|
||||
|
||||
setNotebookParameter(notebook);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('leaves other query parameters in place', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
search: '?foo=bar',
|
||||
};
|
||||
const notebook = '00_quick_start';
|
||||
const expectedUrl =
|
||||
'https://my-kibana.elastic.co/foo/app/elasticsearch?foo=bar¬ebookId=AzD6EcFcEsGMGtQGcAuBDATioA';
|
||||
|
||||
setNotebookParameter(notebook);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('works with hash routes', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
hash: '#/home',
|
||||
};
|
||||
const notebook = '00_quick_start';
|
||||
const expectedUrl =
|
||||
'https://my-kibana.elastic.co/foo/app/elasticsearch#/home?notebookId=AzD6EcFcEsGMGtQGcAuBDATioA';
|
||||
|
||||
setNotebookParameter(notebook);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('removeNotebookParameter', () => {
|
||||
it('leaves other params in place', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
search: `?foo=bar¬ebookId=AzD6EcFcEsGMGtQGcAuBDATioA`,
|
||||
};
|
||||
|
||||
const expectedUrl = 'https://my-kibana.elastic.co/foo/app/elasticsearch?foo=bar';
|
||||
|
||||
removeNotebookParameter();
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('leaves other params with a hashroute', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
hash: `#/home?foo=bar¬ebookId=AzD6EcFcEsGMGtQGcAuBDATioA`,
|
||||
};
|
||||
|
||||
const expectedUrl = 'https://my-kibana.elastic.co/foo/app/elasticsearch#/home?foo=bar';
|
||||
|
||||
removeNotebookParameter();
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('removes ? if load_from was the only param', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
search: `?notebookId=AzD6EcFcEsGMGtQGcAuBDATioA`,
|
||||
};
|
||||
|
||||
const expectedUrl = 'https://my-kibana.elastic.co/foo/app/elasticsearch';
|
||||
|
||||
removeNotebookParameter();
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('removes ? if load_from was the only param in a hashroute', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
hash: '#/home?notebookId=AzD6EcFcEsGMGtQGcAuBDATioA',
|
||||
};
|
||||
|
||||
const expectedUrl = 'https://my-kibana.elastic.co/foo/app/elasticsearch#/home';
|
||||
|
||||
removeNotebookParameter();
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledTimes(1);
|
||||
expect(mockWindow.history.pushState).toHaveBeenCalledWith(
|
||||
{
|
||||
path: expectedUrl,
|
||||
},
|
||||
'',
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
it('noop if load_from not currently defined on QS', () => {
|
||||
mockWindow.location = {
|
||||
...mockWindow.location,
|
||||
pathname: '/foo/app/elasticsearch',
|
||||
hash: `#/home?foo=bar`,
|
||||
};
|
||||
|
||||
removeNotebookParameter();
|
||||
expect(mockWindow.history.pushState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 qs from 'query-string';
|
||||
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';
|
||||
|
||||
function getBaseUrl() {
|
||||
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
|
||||
}
|
||||
|
||||
function parseQueryString() {
|
||||
const [hashRoute, queryString] = (window.location.hash || window.location.search || '').split(
|
||||
'?'
|
||||
);
|
||||
|
||||
const parsedQueryString = qs.parse(queryString || '', { sort: false });
|
||||
return {
|
||||
hasHash: !!window.location.hash,
|
||||
hashRoute,
|
||||
queryString: parsedQueryString,
|
||||
};
|
||||
}
|
||||
|
||||
export const setNotebookParameter = (value: string) => {
|
||||
const baseUrl = getBaseUrl();
|
||||
const { hasHash, hashRoute, queryString } = parseQueryString();
|
||||
const notebookId = compressToEncodedURIComponent(value);
|
||||
queryString.notebookId = notebookId;
|
||||
const params = `?${qs.stringify(queryString)}`;
|
||||
const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`;
|
||||
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
};
|
||||
export const removeNotebookParameter = () => {
|
||||
const baseUrl = getBaseUrl();
|
||||
const { hasHash, hashRoute, queryString } = parseQueryString();
|
||||
if (queryString.notebookId) {
|
||||
delete queryString.notebookId;
|
||||
|
||||
const params = Object.keys(queryString).length ? `?${qs.stringify(queryString)}` : '';
|
||||
const newUrl = hasHash ? `${baseUrl}${hashRoute}${params}` : `${baseUrl}${params}`;
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
}
|
||||
};
|
||||
export const readNotebookParameter = (): string | undefined => {
|
||||
const { queryString } = parseQueryString();
|
||||
if (queryString.notebookId && typeof queryString.notebookId === 'string') {
|
||||
try {
|
||||
const notebookId = decompressFromEncodedURIComponent(queryString.notebookId);
|
||||
if (notebookId.length > 0) return notebookId;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue