Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ The following options are available:
},
```

- `uiSwitcherOptions`: List of UI options shown in the settings dropdown, allowing users to switch between different interfaces. Each entry has an `id` (the URL segment of the target app) and a `label` (the display name). Defaults to JupyterLab, Notebook, and Specta as a fallback.

```json
"uiSwitcherOptions": [
{ "id": "lab", "label": "JupyterLab" },
{ "id": "notebooks", "label": "Notebook" },
{ "id": "specta", "label": "Specta" }
]
```

- `perFileConfig`: an object with key is the file path and value is the above configuration, it's used to have different layout/top bar config for each files, for example:

```json
Expand Down
19 changes: 15 additions & 4 deletions demo/jupyter-lite.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
"topBar": {
"title": "Specta Tree"
},
"uiSwitcherOptions": [
{
"id": "lab",
"label": "JupyterLab"
},
{
"id": "notebooks",
"label": "Notebook"
},
{
"id": "specta",
"label": "Specta"
}
],
"defaultLayout": "default",
"perFileConfig": {
"blog.ipynb": {
Expand All @@ -25,9 +39,6 @@
"hideHeader": false
}
},
"disabledExtensions": [
"@jupyterlab/notebook-extension:open-with-no-kernel",
"@jupyterlite/application-extension:opener"
]
"disabledExtensions": ["@jupyterlab/notebook-extension:open-with-no-kernel"]
}
}
47 changes: 37 additions & 10 deletions src/document/factory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { SpectaWidgetFactory } from '../specta_widget_factory';
import {
ISpectaLayoutRegistry,
ISpectaShell,
ISpectaTopbarWidget
ISpectaTopbarWidget,
ISpectaUiSwitcher
} from '../token';
import { isSpectaApp, readSpectaConfig } from '../tool';
import { MenuComponent } from '../topbar/menuComponent';
Expand All @@ -23,6 +24,7 @@ interface IOptions extends DocumentRegistry.IWidgetFactoryOptions {
shell: ISpectaShell;
spectaLayoutRegistry: ISpectaLayoutRegistry;
spectaTopbar: ISpectaTopbarWidget;
uiSwitcher?: ISpectaUiSwitcher | null;
}

export class NotebookGridWidgetFactory extends ABCWidgetFactory<
Expand All @@ -35,6 +37,7 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory<
this._shell = options.shell;
this._themeManager = options.themeManager;
this._spectaTopbar = options.spectaTopbar;
this._uiSwitcher = options.uiSwitcher;
}

protected createNewWidget(
Expand All @@ -52,23 +55,46 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory<
const isSpecta = isSpectaApp();
if (!spectaConfig.hideTopbar) {
const title = <TitleComponent config={spectaConfig.topBar} />;
let topbarWidget: ISpectaTopbarWidget | undefined = undefined;
let localTopbar: TopbarWidget | undefined = undefined;

if (!isSpecta) {
localTopbar = new TopbarWidget({
config: spectaConfig.topBar
});
topbarWidget = localTopbar;
} else {
topbarWidget = this._spectaTopbar;
}

const menu = (
<MenuComponent
config={spectaConfig.topBar}
themeManager={this._themeManager}
settingsWidgets={topbarWidget?.settingsWidgets}
uiSwitcher={this._uiSwitcher}
currentPath={path}
currentUi={isSpecta ? 'specta' : 'lab'}
/>
);
if (!isSpecta) {
// Not a specta app, add topbar to document widget
const topbar = new TopbarWidget({
config: spectaConfig.topBar
});
topbar.addReactWidget(title, 'left', 0);
topbar.addReactWidget(menu, 'right', 10000);
content.addWidget(topbar);

if (!isSpecta && localTopbar) {
const titleWidget = localTopbar.addReactWidget(title, 'left', 0);
if (titleWidget) {
titleWidget.addClass('specta-topbar-title-wrapper');
}
localTopbar.addReactWidget(menu, 'right', 10000);
content.addWidget(localTopbar);
} else {
if (this._spectaTopbar.addReactWidget) {
this._spectaTopbar.addReactWidget(title, 'left', 0);
const titleWidget = this._spectaTopbar.addReactWidget(
title,
'left',
0
);
if (titleWidget) {
titleWidget.addClass('specta-topbar-title-wrapper');
}
this._spectaTopbar.addReactWidget(menu, 'right', 10000);
}
}
Expand Down Expand Up @@ -97,4 +123,5 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory<
private _shell: ISpectaShell;
private _themeManager?: IThemeManager;
private _spectaTopbar: ISpectaTopbarWidget;
private _uiSwitcher?: ISpectaUiSwitcher | null;
}
61 changes: 43 additions & 18 deletions src/document/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ import {
ISpectaShell,
ISpectaTopbarWidget,
ISpectaTopbarWidgetToken,
ISpectaUiSwitcher,
ISpectaUiSwitcherToken,
ISpectaUrlFactory,
ISpectaUrlFactoryToken
ISpectaUrlFactoryToken,
IUiOption
} from '../token';
import {
configLabLayout,
createFileBrowser,
hideAppLoadingIndicator,
isSpectaApp,
readSpectaConfig,
registerDocumentFactory,
getSpectaDocInfo,
openDocument
Expand All @@ -46,7 +47,8 @@ const activate = (
spectaLayoutRegistry: ISpectaLayoutRegistry,
themeManager: IThemeManager,
spectaTopbar: ISpectaTopbarWidget,
kernelSpecManager: KernelSpec.IManager
kernelSpecManager: KernelSpec.IManager,
uiSwitcher: ISpectaUiSwitcher | null
): IWidgetTracker => {
const namespace = 'specta';
const spectaTracker = new WidgetTracker<Widget>({ namespace });
Expand All @@ -62,7 +64,8 @@ const activate = (
spectaLayoutRegistry,
themeManager,
spectaTopbar,
kernelSpecManager
kernelSpecManager,
uiSwitcher
});

return spectaTracker;
Expand All @@ -84,6 +87,7 @@ export const spectaDocument: JupyterFrontEndPlugin<
ISpectaTopbarWidgetToken,
IKernelSpecManager
],
optional: [ISpectaUiSwitcherToken],
activate,
provides: ISpectaDocTracker
};
Expand All @@ -93,14 +97,23 @@ export const spectaUrlFactory: JupyterFrontEndPlugin<ISpectaUrlFactory> = {
autoStart: true,
provides: ISpectaUrlFactoryToken,
activate: () => {
const urlFactory = (path: string) => {
const urlFactory = (path: string, ui = 'specta'): string => {
const baseUrl = PageConfig.getBaseUrl();
let appUrl = PageConfig.getOption('appUrl');
if (!appUrl.endsWith('/')) {
appUrl = `${appUrl}/`;
}
const url = new URL(URLExt.join(baseUrl, appUrl));
url.searchParams.set('path', path);
// Replace last segment of app URL with target UI
// (e.g. /foo/specta/ to /foo/lab/)
const parts = appUrl.split('/').filter(Boolean);
if (parts.length > 0) {
parts[parts.length - 1] = ui;
} else {
parts.push(ui);
}
const url = new URL(URLExt.join(baseUrl, '/' + parts.join('/') + '/'));
// spectaOpener in lab mode reads 'specta-path'; specta app reads 'path'
url.searchParams.set(ui === 'lab' ? 'specta-path' : 'path', path);
const queries = PageConfig.getOption('query').split('&').filter(Boolean);
queries.forEach(query => {
const [key, value] = query.split('=');
Expand All @@ -112,6 +125,28 @@ export const spectaUrlFactory: JupyterFrontEndPlugin<ISpectaUrlFactory> = {
}
};

export const spectaUiSwitcher: JupyterFrontEndPlugin<ISpectaUiSwitcher> = {
id: 'specta/application-extension:uiSwitcher',
autoStart: true,
requires: [ISpectaUrlFactoryToken],
provides: ISpectaUiSwitcherToken,
activate: (_app, urlFactory: ISpectaUrlFactory) => {
const rawConfig = PageConfig.getOption('spectaConfig');
const globalConfig = rawConfig ? JSON.parse(rawConfig) : {};
const uis: IUiOption[] = globalConfig.uiSwitcherOptions ?? [
{ id: 'lab', label: 'JupyterLab' },
{ id: 'notebooks', label: 'Notebook' },
{ id: 'specta', label: 'Specta' }
];
return {
uis,
switchTo: (path: string, ui: string) => {
window.open(urlFactory(path, ui), '_blank');
}
};
}
};

export const spectaOpener: JupyterFrontEndPlugin<void, ILabShell> = {
id: 'specta/application-extension:opener',
autoStart: true,
Expand Down Expand Up @@ -140,18 +175,8 @@ export const spectaOpener: JupyterFrontEndPlugin<void, ILabShell> = {
return;
}
app.restored.then(async () => {
const labShell = app.shell;

const { isSpectaDoc, factory } = getSpectaDocInfo(path, app);

if (isSpectaDoc) {
const commands = app.commands;
const spectaConfig = readSpectaConfig({});
await configLabLayout({
config: spectaConfig.labConfig,
labShell,
commands
});
openDocument(path, factory, docManager, app.shell);
}
});
Expand Down
4 changes: 3 additions & 1 deletion src/extension_plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
spectaDocument,
spectaOpener,
spectaUiSwitcher,
spectaUrlFactory
} from './document/plugin';
import { spectaLayoutRegistry } from './layout';
Expand All @@ -17,5 +18,6 @@ export default [
topbarPlugin,
appMeta,
cellMeta,
spectaUrlFactory
spectaUrlFactory,
spectaUiSwitcher
];
25 changes: 22 additions & 3 deletions src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface ISpectaAppConfig {
slidesTheme?: string;
loadingName?: string;
executionDelay?: number;
uiSwitcherOptions?: IUiOption[];
labConfig?: {
setSingleMode?: boolean;
hideLeftPanel?: boolean;
Expand All @@ -65,7 +66,15 @@ export interface ISpectaCellConfig {
showOutput?: boolean;
outputSize?: 'Small' | 'Big' | 'Full';
}
export type ISpectaUrlFactory = (path: string) => string;
export interface IUiOption {
id: string;
label: string;
}
export type ISpectaUrlFactory = (path: string, ui?: string) => string;
export interface ISpectaUiSwitcher {
uis: IUiOption[];
switchTo: (path: string, ui: string) => void;
}
export const ISpectaLayoutRegistry = new Token<ISpectaLayoutRegistry>(
'specta:ISpectaLayoutRegistry'
);
Expand All @@ -77,18 +86,28 @@ export const ISpectaDocTracker = new Token<IWidgetTracker<Widget>>(
export const ISpectaUrlFactoryToken = new Token<ISpectaUrlFactory>(
'specta:ISpectaUrlFactoryToken'
);
export const ISpectaUiSwitcherToken = new Token<ISpectaUiSwitcher>(
'specta:ISpectaUiSwitcherToken'
);

export interface ISpectaWidget {
readonly node: HTMLElement;
readonly isAttached: boolean;
}

export interface ISpectaTopbarWidget {
addTopbarWidget?: (
widget: Widget,
widget: ISpectaWidget,
side: 'left' | 'right',
rank: number
) => void;
addReactWidget?: (
widget: JSX.Element,
side: 'left' | 'right',
rank: number
) => void;
) => Widget;
addSettingsWidget?: (widget: ISpectaWidget) => void;
settingsWidgets?: ISpectaWidget[];
}
export const ISpectaTopbarWidgetToken = new Token<ISpectaTopbarWidget>(
'specta:ISpectaTopbarWidget'
Expand Down
11 changes: 8 additions & 3 deletions src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ISpectaLayoutRegistry,
ISpectaShell,
ISpectaTopbarWidget,
ISpectaUiSwitcher,
ISpectaUrlFactory
} from './token';

Expand Down Expand Up @@ -57,6 +58,7 @@ export function registerDocumentFactory(options: {
themeManager?: IThemeManager;
spectaTopbar: ISpectaTopbarWidget;
kernelSpecManager: KernelSpec.IManager;
uiSwitcher?: ISpectaUiSwitcher | null;
}) {
const {
factoryName,
Expand All @@ -69,7 +71,8 @@ export function registerDocumentFactory(options: {
spectaLayoutRegistry,
themeManager,
spectaTopbar,
kernelSpecManager
kernelSpecManager,
uiSwitcher
} = options;

const spectaWidgetFactory = new SpectaWidgetFactory({
Expand All @@ -90,7 +93,8 @@ export function registerDocumentFactory(options: {
spectaWidgetFactory,
themeManager,
spectaLayoutRegistry,
spectaTopbar
spectaTopbar,
uiSwitcher
});

// Registering the widget factory
Expand Down Expand Up @@ -134,7 +138,8 @@ export function registerDocumentFactory(options: {
spectaWidgetFactory,
themeManager,
spectaLayoutRegistry,
spectaTopbar
spectaTopbar,
uiSwitcher
});

app.docRegistry.addWidgetFactory(plainbWidgetFactory);
Expand Down
Loading
Loading