mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Search][Notebooks] Introduce support for lists (#187163)
## Summary This PR introduces support for notebook lists that can be set via query parameter or via a function exported from the `search_notebooks` plugin start object. This will be used to allow setting a different list of notebooks for specific pages that allow the notebooks shown to be contextual to the current user task. This PR does not add any new lists of notebooks, as that will be done in another PR for the static notebooks and done via the S3 file for dynamic notebooks. ### 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
This commit is contained in:
parent
4e785c669c
commit
bda4a6282e
13 changed files with 371 additions and 38 deletions
|
@ -17,8 +17,12 @@ export interface NotebookInformation {
|
|||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type NotebookCatalogResponse = Pick<NotebookCatalog, 'notebooks'>;
|
||||
|
||||
export interface NotebookCatalog {
|
||||
notebooks: NotebookInformation[];
|
||||
lists?: Record<string, string[] | undefined>;
|
||||
}
|
||||
|
||||
export interface Notebook extends NotebookInformation {
|
||||
|
@ -47,6 +51,9 @@ export const NotebookCatalogSchema = schema.object(
|
|||
),
|
||||
{ minSize: 1 }
|
||||
),
|
||||
lists: schema.maybe(
|
||||
schema.recordOf(schema.string(), schema.arrayOf(schema.string(), { minSize: 1 }))
|
||||
),
|
||||
},
|
||||
{
|
||||
unknowns: 'allow',
|
||||
|
|
|
@ -10,7 +10,23 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EmbeddedConsoleViewButtonProps } from '@kbn/console-plugin/public';
|
||||
|
||||
export const SearchNotebooksButton = ({ activeView, onClick }: EmbeddedConsoleViewButtonProps) => {
|
||||
export interface SearchNotebooksButtonProps extends EmbeddedConsoleViewButtonProps {
|
||||
clearNotebookList: () => void;
|
||||
}
|
||||
|
||||
export const SearchNotebooksButton = ({
|
||||
activeView,
|
||||
onClick,
|
||||
clearNotebookList,
|
||||
}: SearchNotebooksButtonProps) => {
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
// When the Notebooks button is unmounted we want to clear
|
||||
// any page specific contextual notebook list that was set.
|
||||
clearNotebookList();
|
||||
};
|
||||
}, [clearNotebookList]);
|
||||
|
||||
if (activeView) {
|
||||
return (
|
||||
<EuiButton
|
||||
|
|
|
@ -11,15 +11,21 @@ import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { SearchNotebooks } from './search_notebooks';
|
||||
import { NotebookListValue } from '../types';
|
||||
|
||||
export interface SearchNotebooksViewProps {
|
||||
core: CoreStart;
|
||||
queryClient: QueryClient;
|
||||
getNotebookList: () => NotebookListValue;
|
||||
}
|
||||
|
||||
export const SearchNotebooksView = ({ core, queryClient }: SearchNotebooksViewProps) => (
|
||||
export const SearchNotebooksView = ({
|
||||
core,
|
||||
queryClient,
|
||||
getNotebookList,
|
||||
}: SearchNotebooksViewProps) => (
|
||||
<KibanaThemeProvider theme={core.theme}>
|
||||
<KibanaContextProvider services={{ ...core }}>
|
||||
<KibanaContextProvider services={{ ...core, notebooks: { getNotebookList } }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SearchNotebooks />
|
||||
</QueryClientProvider>
|
||||
|
|
|
@ -7,22 +7,38 @@
|
|||
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { EmbeddedConsoleView } from '@kbn/console-plugin/public';
|
||||
import type {
|
||||
EmbeddedConsoleView,
|
||||
EmbeddedConsoleViewButtonProps,
|
||||
} from '@kbn/console-plugin/public';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { SearchNotebooksButton } from './components/notebooks_button';
|
||||
import { NotebookListValue } from './types';
|
||||
|
||||
const SearchNotebooksButton = dynamic(async () => ({
|
||||
default: (await import('./components/notebooks_button')).SearchNotebooksButton,
|
||||
}));
|
||||
const SearchNotebooksView = dynamic(async () => ({
|
||||
default: (await import('./components/notebooks_view')).SearchNotebooksView,
|
||||
}));
|
||||
|
||||
export const notebooksConsoleView = (
|
||||
core: CoreStart,
|
||||
queryClient: QueryClient
|
||||
queryClient: QueryClient,
|
||||
clearNotebookList: () => void,
|
||||
getNotebookListValue: () => NotebookListValue
|
||||
): EmbeddedConsoleView => {
|
||||
return {
|
||||
ActivationButton: SearchNotebooksButton,
|
||||
ViewContent: () => <SearchNotebooksView core={core} queryClient={queryClient} />,
|
||||
ActivationButton: (props: EmbeddedConsoleViewButtonProps) => (
|
||||
<SearchNotebooksButton {...props} clearNotebookList={clearNotebookList} />
|
||||
),
|
||||
ViewContent: () => (
|
||||
<SearchNotebooksView
|
||||
core={core}
|
||||
queryClient={queryClient}
|
||||
getNotebookList={getNotebookListValue}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,8 +9,13 @@ 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';
|
||||
|
||||
import { NotebookListValue } from '../types';
|
||||
|
||||
export interface SearchNotebooksContext {
|
||||
console: ConsolePluginStart;
|
||||
notebooks: {
|
||||
getNotebookList: () => NotebookListValue;
|
||||
};
|
||||
}
|
||||
|
||||
type ServerlessSearchKibanaContext = CoreStart & SearchNotebooksContext;
|
||||
|
|
|
@ -6,13 +6,20 @@
|
|||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { NotebookCatalog } from '../../common/types';
|
||||
import { NotebookCatalogResponse } from '../../common/types';
|
||||
import { useKibanaServices } from './use_kibana';
|
||||
import { useNotebookList } from './use_notebooks_list';
|
||||
|
||||
export const useNotebooksCatalog = () => {
|
||||
const { http } = useKibanaServices();
|
||||
const list = useNotebookList();
|
||||
return useQuery({
|
||||
queryKey: ['fetchNotebooksCatalog'],
|
||||
queryFn: () => http.get<NotebookCatalog>('/internal/search_notebooks/notebooks'),
|
||||
queryKey: [`fetchNotebooksCatalog-${list ?? 'default'}`],
|
||||
queryFn: () =>
|
||||
http.get<NotebookCatalogResponse>('/internal/search_notebooks/notebooks', {
|
||||
query: {
|
||||
list,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { parse } from 'query-string';
|
||||
import { useKibanaServices } from './use_kibana';
|
||||
|
||||
export const readNotebookListFromParam = () => {
|
||||
const [, queryString] = (window.location.search || window.location.hash || '').split('?');
|
||||
|
||||
const queryParams = parse(queryString || '', { sort: false });
|
||||
if (queryParams && queryParams.nblist && typeof queryParams.nblist === 'string') {
|
||||
return queryParams.nblist;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const useNotebookList = () => {
|
||||
const {
|
||||
notebooks: { getNotebookList },
|
||||
} = useKibanaServices();
|
||||
const nbList = useMemo(() => getNotebookList(), [getNotebookList]);
|
||||
const nbListQueryParam = useMemo(() => readNotebookListFromParam(), []);
|
||||
|
||||
if (nbListQueryParam) return nbListQueryParam;
|
||||
if (nbList) return nbList;
|
||||
return undefined;
|
||||
};
|
|
@ -13,13 +13,16 @@ import {
|
|||
SearchNotebooksPluginSetup,
|
||||
SearchNotebooksPluginStart,
|
||||
SearchNotebooksPluginStartDependencies,
|
||||
NotebookListValue,
|
||||
} from './types';
|
||||
import { getErrorCode, getErrorMessage, isKibanaServerError } from './utils/get_error_message';
|
||||
|
||||
export class SearchNotebooksPlugin
|
||||
implements Plugin<SearchNotebooksPluginSetup, SearchNotebooksPluginStart>
|
||||
{
|
||||
private notebooksList: NotebookListValue = null;
|
||||
private queryClient: QueryClient | undefined;
|
||||
|
||||
public setup(core: CoreSetup): SearchNotebooksPluginSetup {
|
||||
this.queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
|
@ -55,10 +58,31 @@ export class SearchNotebooksPlugin
|
|||
): SearchNotebooksPluginStart {
|
||||
if (deps.console?.registerEmbeddedConsoleAlternateView) {
|
||||
deps.console.registerEmbeddedConsoleAlternateView(
|
||||
notebooksConsoleView(core, this.queryClient!)
|
||||
notebooksConsoleView(
|
||||
core,
|
||||
this.queryClient!,
|
||||
this.clearNotebookList.bind(this),
|
||||
this.getNotebookList.bind(this)
|
||||
)
|
||||
);
|
||||
}
|
||||
return {};
|
||||
return {
|
||||
setNotebookList: (value: NotebookListValue) => {
|
||||
this.setNotebookList(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
public stop() {}
|
||||
|
||||
private clearNotebookList() {
|
||||
this.setNotebookList(null);
|
||||
}
|
||||
|
||||
private setNotebookList(value: NotebookListValue) {
|
||||
this.notebooksList = value;
|
||||
}
|
||||
|
||||
private getNotebookList(): NotebookListValue {
|
||||
return this.notebooksList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,9 +9,13 @@ 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 SearchNotebooksPluginStart {
|
||||
setNotebookList: (value: NotebookListValue) => void;
|
||||
}
|
||||
|
||||
export interface SearchNotebooksPluginStartDependencies {
|
||||
console: ConsolePluginStart;
|
||||
}
|
||||
|
||||
export type NotebookListValue = string | null;
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
getNotebook,
|
||||
getNotebookCatalog,
|
||||
DEFAULT_NOTEBOOKS,
|
||||
NOTEBOOKS_MAP,
|
||||
NotebookCatalogFetchOptions,
|
||||
getNotebookMetadata,
|
||||
} from './notebook_catalog';
|
||||
|
@ -65,7 +66,32 @@ describe('Notebook Catalog', () => {
|
|||
describe('getNotebookCatalog', () => {
|
||||
describe('static notebooks', () => {
|
||||
it('returns default notebooks when theres an empty catalog config', async () => {
|
||||
await expect(getNotebookCatalog(staticOptions)).resolves.toBe(DEFAULT_NOTEBOOKS);
|
||||
await expect(getNotebookCatalog(staticOptions)).resolves.toMatchObject(DEFAULT_NOTEBOOKS);
|
||||
});
|
||||
it.skip('returns requested list of notebooks when it exists', async () => {
|
||||
// Re-enable this with actual list when we implement them
|
||||
await expect(
|
||||
getNotebookCatalog({ ...staticOptions, notebookList: 'ml' })
|
||||
).resolves.toMatchObject({
|
||||
notebooks: [
|
||||
NOTEBOOKS_MAP['03_elser'],
|
||||
NOTEBOOKS_MAP['02_hybrid_search'],
|
||||
NOTEBOOKS_MAP['04_multilingual'],
|
||||
],
|
||||
});
|
||||
});
|
||||
it('returns default list if requested list doesnt exist', async () => {
|
||||
await expect(
|
||||
getNotebookCatalog({ ...staticOptions, notebookList: 'foo' })
|
||||
).resolves.toMatchObject({
|
||||
notebooks: [
|
||||
NOTEBOOKS_MAP['00_quick_start'],
|
||||
NOTEBOOKS_MAP['01_keyword_querying_filtering'],
|
||||
NOTEBOOKS_MAP['02_hybrid_search'],
|
||||
NOTEBOOKS_MAP['03_elser'],
|
||||
NOTEBOOKS_MAP['04_multilingual'],
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -310,6 +336,97 @@ describe('Notebook Catalog', () => {
|
|||
timestamp: fakeNow,
|
||||
});
|
||||
});
|
||||
describe('supports notebook lists', () => {
|
||||
beforeEach(() => {
|
||||
const mockCatalog: RemoteNotebookCatalog = {
|
||||
notebooks: [
|
||||
{
|
||||
id: 'unit-test',
|
||||
title: 'Test',
|
||||
description: 'Test notebook',
|
||||
url: 'http://localhost:3000/my_notebook.ipynb',
|
||||
},
|
||||
{
|
||||
id: 'unit-test-002',
|
||||
title: 'Test',
|
||||
description: 'Test notebook 2',
|
||||
url: 'http://localhost:3000/my_other_notebook.ipynb',
|
||||
},
|
||||
],
|
||||
lists: {
|
||||
default: ['unit-test', 'unit-test-002'],
|
||||
test: ['unit-test-002'],
|
||||
vector: ['unit-test'],
|
||||
},
|
||||
};
|
||||
const mockResp = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue(mockCatalog),
|
||||
};
|
||||
fetchMock.mockResolvedValue(mockResp);
|
||||
});
|
||||
|
||||
it('can return a custom notebook list', async () => {
|
||||
await expect(
|
||||
getNotebookCatalog({ ...dynamicOptions, notebookList: 'test' })
|
||||
).resolves.toEqual({
|
||||
notebooks: [
|
||||
{
|
||||
id: 'unit-test-002',
|
||||
title: 'Test',
|
||||
description: 'Test notebook 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(
|
||||
getNotebookCatalog({ ...dynamicOptions, notebookList: 'vector' })
|
||||
).resolves.toEqual({
|
||||
notebooks: [
|
||||
{
|
||||
id: 'unit-test',
|
||||
title: 'Test',
|
||||
description: 'Test notebook',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it('returns default list when requested list not defined', async () => {
|
||||
await expect(
|
||||
getNotebookCatalog({ ...dynamicOptions, notebookList: 'foo' })
|
||||
).resolves.toEqual({
|
||||
notebooks: [
|
||||
{
|
||||
id: 'unit-test',
|
||||
title: 'Test',
|
||||
description: 'Test notebook',
|
||||
},
|
||||
{
|
||||
id: 'unit-test-002',
|
||||
title: 'Test',
|
||||
description: 'Test notebook 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it('returns default list when list is not specified', async () => {
|
||||
await expect(getNotebookCatalog(dynamicOptions)).resolves.toEqual({
|
||||
notebooks: [
|
||||
{
|
||||
id: 'unit-test',
|
||||
title: 'Test',
|
||||
description: 'Test notebook',
|
||||
},
|
||||
{
|
||||
id: 'unit-test-002',
|
||||
title: 'Test',
|
||||
description: 'Test notebook 2',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ import { NotebookDefinition } from '@kbn/ipynb';
|
|||
|
||||
import {
|
||||
NotebookCatalog,
|
||||
NotebookCatalogResponse,
|
||||
NotebookInformation,
|
||||
NotebookCatalogSchema,
|
||||
NotebookSchema,
|
||||
} from '../../common/types';
|
||||
|
||||
|
@ -23,9 +23,10 @@ import type { SearchNotebooksConfig } from '../config';
|
|||
import type { NotebooksCache, RemoteNotebookCatalog } from '../types';
|
||||
import {
|
||||
cleanCachedNotebook,
|
||||
cleanCachedNotebookCatalog,
|
||||
cleanNotebookMetadata,
|
||||
dateWithinTTL,
|
||||
notebookCatalogResponse,
|
||||
validateRemoteNotebookCatalog,
|
||||
} from '../utils';
|
||||
|
||||
const NOTEBOOKS_DATA_DIR = '../data';
|
||||
|
@ -38,8 +39,10 @@ export interface NotebookCatalogFetchOptions {
|
|||
cache: NotebooksCache;
|
||||
config: SearchNotebooksConfig;
|
||||
logger: Logger;
|
||||
notebookList?: string;
|
||||
}
|
||||
|
||||
// Notebook catalog v1, leaving to ensure backward-compatibility
|
||||
export const DEFAULT_NOTEBOOKS: NotebookCatalog = {
|
||||
notebooks: [
|
||||
{
|
||||
|
@ -102,26 +105,40 @@ export const DEFAULT_NOTEBOOKS: NotebookCatalog = {
|
|||
},
|
||||
],
|
||||
};
|
||||
// Notebook catalog v1.1 with lists for contextual notebooks
|
||||
export const DEFAULT_NOTEBOOK_CATALOG: NotebookCatalog = {
|
||||
notebooks: [...DEFAULT_NOTEBOOKS.notebooks],
|
||||
lists: {
|
||||
default: [
|
||||
'00_quick_start',
|
||||
'01_keyword_querying_filtering',
|
||||
'02_hybrid_search',
|
||||
'03_elser',
|
||||
'04_multilingual',
|
||||
],
|
||||
},
|
||||
};
|
||||
export const NOTEBOOKS_MAP: Record<string, NotebookInformation> =
|
||||
DEFAULT_NOTEBOOKS.notebooks.reduce((nbMap, nb) => {
|
||||
DEFAULT_NOTEBOOK_CATALOG.notebooks.reduce((nbMap, nb) => {
|
||||
nbMap[nb.id] = nb;
|
||||
return nbMap;
|
||||
}, {} as Record<string, NotebookInformation>);
|
||||
|
||||
const NOTEBOOK_IDS = DEFAULT_NOTEBOOKS.notebooks.map(({ id }) => id);
|
||||
const NOTEBOOK_IDS = DEFAULT_NOTEBOOK_CATALOG.notebooks.map(({ id }) => id);
|
||||
|
||||
export const getNotebookCatalog = async ({
|
||||
config,
|
||||
cache,
|
||||
logger,
|
||||
}: NotebookCatalogFetchOptions) => {
|
||||
notebookList,
|
||||
}: NotebookCatalogFetchOptions): Promise<NotebookCatalogResponse> => {
|
||||
if (config.catalog && config.catalog.url) {
|
||||
const catalog = await fetchNotebookCatalog(config.catalog, cache, logger);
|
||||
const catalog = await fetchNotebookCatalog(config.catalog, cache, logger, notebookList);
|
||||
if (catalog) {
|
||||
return catalog;
|
||||
}
|
||||
}
|
||||
return DEFAULT_NOTEBOOKS;
|
||||
return notebookCatalogResponse(DEFAULT_NOTEBOOK_CATALOG, notebookList);
|
||||
};
|
||||
|
||||
export const getNotebook = async (
|
||||
|
@ -179,19 +196,20 @@ type CatalogConfig = Readonly<{
|
|||
export const fetchNotebookCatalog = async (
|
||||
catalogConfig: CatalogConfig,
|
||||
cache: NotebooksCache,
|
||||
logger: Logger
|
||||
): Promise<NotebookCatalog | null> => {
|
||||
logger: Logger,
|
||||
notebookList?: string
|
||||
): Promise<NotebookCatalogResponse | null> => {
|
||||
if (cache.catalog && dateWithinTTL(cache.catalog.timestamp, catalogConfig.ttl)) {
|
||||
return cleanCachedNotebookCatalog(cache.catalog);
|
||||
return notebookCatalogResponse(cache.catalog, notebookList);
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(catalogConfig.url, FETCH_OPTIONS);
|
||||
if (resp.ok) {
|
||||
const respJson = await resp.json();
|
||||
const catalog: RemoteNotebookCatalog = NotebookCatalogSchema.validate(respJson);
|
||||
const catalog: RemoteNotebookCatalog = validateRemoteNotebookCatalog(respJson);
|
||||
cache.catalog = { ...catalog, timestamp: new Date() };
|
||||
return cleanCachedNotebookCatalog(cache.catalog);
|
||||
return notebookCatalogResponse(cache.catalog, notebookList);
|
||||
} else {
|
||||
throw new Error(`Failed to fetch notebook ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
|
@ -203,7 +221,7 @@ export const fetchNotebookCatalog = async (
|
|||
if (cache.catalog && dateWithinTTL(cache.catalog.timestamp, catalogConfig.errorTTL)) {
|
||||
// If we can't fetch the catalog but we have it cached and it's within the error TTL,
|
||||
// returned the cached value.
|
||||
return cleanCachedNotebookCatalog(cache.catalog);
|
||||
return notebookCatalogResponse(cache.catalog, notebookList);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,16 +16,26 @@ export function defineRoutes({ config, notebooksCache, logger, router }: RouteDe
|
|||
router.get(
|
||||
{
|
||||
path: '/internal/search_notebooks/notebooks',
|
||||
validate: {},
|
||||
validate: {
|
||||
query: schema.object({
|
||||
list: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
},
|
||||
async (_context, _request, response) => {
|
||||
const notebooks = await getNotebookCatalog({ cache: notebooksCache, config, logger });
|
||||
async (_context, request, response) => {
|
||||
const { list } = request.query;
|
||||
const resp = await getNotebookCatalog({
|
||||
cache: notebooksCache,
|
||||
config,
|
||||
logger,
|
||||
notebookList: list,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: notebooks,
|
||||
body: resp,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,11 +6,17 @@
|
|||
*/
|
||||
|
||||
import { NotebookDefinition } from '@kbn/ipynb';
|
||||
import { NotebookCatalog, NotebookInformation } from '../common/types';
|
||||
import {
|
||||
NotebookCatalog,
|
||||
NotebookCatalogResponse,
|
||||
NotebookCatalogSchema,
|
||||
NotebookInformation,
|
||||
} from '../common/types';
|
||||
import type {
|
||||
CachedNotebook,
|
||||
CachedNotebookCatalog,
|
||||
NotebooksCache,
|
||||
RemoteNotebookCatalog,
|
||||
RemoteNotebookInformation,
|
||||
} from './types';
|
||||
|
||||
|
@ -24,10 +30,12 @@ export function dateWithinTTL(value: Date, ttl: number) {
|
|||
return (Date.now() - value.getTime()) / 1000 <= ttl;
|
||||
}
|
||||
|
||||
export function cleanCachedNotebookCatalog(catalog: CachedNotebookCatalog): NotebookCatalog {
|
||||
const notebooks = catalog.notebooks.map(cleanNotebookMetadata);
|
||||
export function cleanCachedNotebookCatalog(
|
||||
notebooks: Array<RemoteNotebookInformation | NotebookInformation>
|
||||
): NotebookCatalogResponse {
|
||||
const cleanedNotebooks = notebooks.map(cleanNotebookMetadata);
|
||||
return {
|
||||
notebooks,
|
||||
notebooks: cleanedNotebooks,
|
||||
};
|
||||
}
|
||||
export function cleanCachedNotebook(notebook: CachedNotebook): NotebookDefinition {
|
||||
|
@ -35,7 +43,9 @@ export function cleanCachedNotebook(notebook: CachedNotebook): NotebookDefinitio
|
|||
return result;
|
||||
}
|
||||
|
||||
export function cleanNotebookMetadata(nb: RemoteNotebookInformation): NotebookInformation {
|
||||
export function cleanNotebookMetadata(
|
||||
nb: RemoteNotebookInformation | NotebookInformation
|
||||
): NotebookInformation {
|
||||
const { id, title, description, link } = nb;
|
||||
return {
|
||||
description,
|
||||
|
@ -44,3 +54,64 @@ export function cleanNotebookMetadata(nb: RemoteNotebookInformation): NotebookIn
|
|||
title,
|
||||
};
|
||||
}
|
||||
|
||||
export function isCachedNotebookCatalog(
|
||||
catalog: CachedNotebookCatalog | NotebookCatalog
|
||||
): catalog is CachedNotebookCatalog {
|
||||
return 'timestamp' in catalog;
|
||||
}
|
||||
|
||||
const DEFAULT_NOTEBOOK_LIST_KEY = 'default';
|
||||
export function notebookCatalogResponse(
|
||||
catalog: CachedNotebookCatalog | NotebookCatalog,
|
||||
list: string = DEFAULT_NOTEBOOK_LIST_KEY
|
||||
): NotebookCatalogResponse {
|
||||
if (!catalog.lists) {
|
||||
return isCachedNotebookCatalog(catalog)
|
||||
? cleanCachedNotebookCatalog(catalog.notebooks)
|
||||
: catalog;
|
||||
}
|
||||
|
||||
const listOfNotebookIds = getListOfNotebookIds(catalog.lists, list);
|
||||
const notebookIndexMap = (catalog.notebooks as NotebookInformation[]).reduce(
|
||||
(indexMap, nb, i) => {
|
||||
indexMap[nb.id] = i;
|
||||
return indexMap;
|
||||
},
|
||||
{} as Record<string, number | undefined>
|
||||
);
|
||||
const notebooks = listOfNotebookIds
|
||||
.map((id) => {
|
||||
const nbIndex = notebookIndexMap[id];
|
||||
if (nbIndex === undefined) return undefined;
|
||||
return catalog.notebooks[nbIndex] ?? undefined;
|
||||
})
|
||||
.filter(
|
||||
(nbInfo): nbInfo is RemoteNotebookInformation | NotebookInformation => nbInfo !== undefined
|
||||
);
|
||||
return cleanCachedNotebookCatalog(notebooks);
|
||||
}
|
||||
|
||||
function getListOfNotebookIds(
|
||||
catalogLists: NonNullable<NotebookCatalog['lists']>,
|
||||
list: string
|
||||
): string[] {
|
||||
if (list in catalogLists && catalogLists[list]) return catalogLists[list]!;
|
||||
if (DEFAULT_NOTEBOOK_LIST_KEY in catalogLists && catalogLists[DEFAULT_NOTEBOOK_LIST_KEY])
|
||||
return catalogLists[DEFAULT_NOTEBOOK_LIST_KEY];
|
||||
|
||||
// This should not happen as we should not load a catalog with lists thats missing the default list as valid,
|
||||
// but handling this case for code completeness.
|
||||
throw new Error('Notebook catalog has lists, but is missing default list'); // TODO: ?translate
|
||||
}
|
||||
|
||||
export function validateRemoteNotebookCatalog(respJson: any): RemoteNotebookCatalog {
|
||||
const catalog: RemoteNotebookCatalog = NotebookCatalogSchema.validate(respJson);
|
||||
if (catalog.lists && !(DEFAULT_NOTEBOOK_LIST_KEY in catalog.lists)) {
|
||||
// TODO: translate error message
|
||||
throw new Error(
|
||||
'Invalid remote notebook catalog. Catalog defines lists, but is missing the default list.'
|
||||
);
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue