Explorar o código

Add a way to manage workspaces in the lab ui

Jean-Baptiste Wons %!s(int64=4) %!d(string=hai) anos
pai
achega
fcb4139cc1

+ 1 - 0
packages/apputils-extension/package.json

@@ -40,6 +40,7 @@
     "@jupyterlab/application": "^3.0.0-alpha.4",
     "@jupyterlab/apputils": "^3.0.0-alpha.4",
     "@jupyterlab/coreutils": "^5.0.0-alpha.4",
+    "@jupyterlab/filebrowser": "^3.0.0-alpha.4",
     "@jupyterlab/mainmenu": "^3.0.0-alpha.4",
     "@jupyterlab/settingregistry": "^3.0.0-alpha.4",
     "@jupyterlab/statedb": "^3.0.0-alpha.4",

+ 18 - 1
packages/apputils-extension/src/index.ts

@@ -21,6 +21,10 @@ import {
   sessionContextDialogs
 } from '@jupyterlab/apputils';
 
+import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
+
+import { IMainMenu } from '@jupyterlab/mainmenu';
+
 import { URLExt } from '@jupyterlab/coreutils';
 
 import { IStateDB, StateDB } from '@jupyterlab/statedb';
@@ -39,6 +43,8 @@ import { settingsPlugin } from './settingsplugin';
 
 import { themesPlugin, themesPaletteMenuPlugin } from './themeplugins';
 
+import { WorkspaceUI } from './workspace-ui';
+
 /**
  * The interval in milliseconds before recover options appear during splash.
  */
@@ -487,6 +493,16 @@ const utilityCommands: JupyterFrontEndPlugin<void> = {
   }
 };
 
+/**
+ * The workspace UI extension.
+ */
+const workspaceUI: JupyterFrontEndPlugin<void> = {
+  activate: WorkspaceUI.activate,
+  id: '@jupyterlab/apputils-extension:workspace-ui',
+  autoStart: true,
+  requires: [IMainMenu, IFileBrowserFactory, IWindowResolver, IStateDB, IRouter]
+};
+
 /**
  * Export the plugins as default.
  */
@@ -501,7 +517,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   sessionDialogs,
   themesPlugin,
   themesPaletteMenuPlugin,
-  utilityCommands
+  utilityCommands,
+  workspaceUI
 ];
 export default plugins;
 

+ 255 - 0
packages/apputils-extension/src/workspace-ui.ts

@@ -0,0 +1,255 @@
+import { JupyterFrontEnd, IRouter } from '@jupyterlab/application';
+import { IMainMenu } from '@jupyterlab/mainmenu';
+import { showDialog, Dialog, IWindowResolver } from '@jupyterlab/apputils';
+import { IFileBrowserFactory, FileBrowser } from '@jupyterlab/filebrowser';
+import {
+  ContentsManager,
+  Workspace,
+  WorkspaceManager
+} from '@jupyterlab/services';
+import { IStateDB } from '@jupyterlab/statedb';
+import { Widget } from '@lumino/widgets';
+import {
+  DocumentRegistry,
+  ABCWidgetFactory,
+  IDocumentWidget,
+  DocumentWidget
+} from '@jupyterlab/docregistry';
+
+namespace CommandIDs {
+  export const saveWorkspaceAsCommandId = 'workspace-ui:save-as';
+  export const saveWorkspaceCommandId = 'workspace-ui:save';
+}
+
+const WORKSPACE_NAME = 'jupyterlab-workspace';
+const WORKSPACE_EXT = '.' + WORKSPACE_NAME;
+const LAST_SAVE_ID = 'workspace-ui:lastSave';
+const ICON_NAME = 'jp-JupyterIcon';
+
+function getDummyWidget(context: DocumentRegistry.Context): IDocumentWidget {
+  const content = new Widget();
+  const widget = new DocumentWidget({ content, context });
+  // Dispose of the content so that it does not actually show
+  content.dispose();
+  return widget;
+}
+
+/**
+ * This widget factory is used to handle double click on workspace
+ */
+class WorkspaceFactory extends ABCWidgetFactory<IDocumentWidget> {
+  workspaces: WorkspaceManager;
+  router: IRouter;
+  state: IStateDB;
+
+  constructor(workspaces: WorkspaceManager, router: IRouter, state: IStateDB) {
+    super({
+      name: 'Workspace loader',
+      fileTypes: [WORKSPACE_NAME],
+      defaultFor: [WORKSPACE_NAME],
+      readOnly: true
+    });
+    this.workspaces = workspaces;
+    this.router = router;
+    this.state = state;
+  }
+
+  protected createNewWidget(
+    context: DocumentRegistry.Context
+  ): IDocumentWidget {
+    // Save workspace description into jupyterlab, and navigate to it when done
+    void context.ready.then(async () => {
+      const workspaceDesc = (context.model.toJSON() as unknown) as Workspace.IWorkspace;
+      const path = context.path;
+
+      const workspaceId = workspaceDesc.metadata.id;
+      // Upload workspace content to jupyterlab
+      await this.workspaces.save(workspaceId, workspaceDesc);
+      // Save last save location, for save button to work
+      await this.state.save(LAST_SAVE_ID, path);
+      this.router.navigate(workspaceId, { hard: true });
+    });
+    return getDummyWidget(context);
+  }
+}
+
+/**
+ * Ask user for a path to save to.
+ * @param defaultPath Path already present when the dialog is shown
+ */
+async function getSavePath(defaultPath: string): Promise<string | null> {
+  const saveBtn = Dialog.okButton({ label: 'Save' });
+  const result = await showDialog({
+    title: 'Save Current Workspace As...',
+    body: new SaveWidget(defaultPath),
+    buttons: [Dialog.cancelButton(), saveBtn]
+  });
+  if (result.button.label === 'Save') {
+    return result.value;
+  } else {
+    return null;
+  }
+}
+
+/**
+ * A widget that gets a file path from a user.
+ */
+class SaveWidget extends Widget {
+  constructor(path: string) {
+    super({ node: createSaveNode(path) });
+  }
+
+  getValue(): string {
+    return (this.node as HTMLInputElement).value;
+  }
+}
+
+/**
+ * Create the node for a save widget.
+ */
+function createSaveNode(path: string): HTMLElement {
+  const input = document.createElement('input');
+  input.value = path;
+  return input;
+}
+
+/**
+ * Save workspace to a user provided location
+ */
+async function save(
+  userPath: string,
+  contents: ContentsManager,
+  data: Promise<Workspace.IWorkspace>,
+  state: IStateDB
+): Promise<void> {
+  let name = userPath.split('/').pop();
+
+  // Add extension if it was not proovided, or remove extention from name if it was
+  if (name !== undefined && name.includes('.')) {
+    name = name.split('.')[0];
+  } else {
+    userPath = userPath + WORKSPACE_EXT;
+  }
+
+  // Save last save location, for save button to work
+  await state.save(LAST_SAVE_ID, userPath);
+
+  const resolvedData = await data;
+  resolvedData.metadata.id = '/lab/workspaces/' + name;
+  await contents.save(userPath, {
+    type: 'file',
+    format: 'text',
+    content: JSON.stringify(resolvedData)
+  });
+}
+
+/**
+ * Ask user for location, and save workspace. Default location is the directory opened in the file browser
+ */
+async function saveAs(
+  browser: FileBrowser,
+  contents: ContentsManager,
+  data: Promise<Workspace.IWorkspace>,
+  state: IStateDB
+): Promise<void> {
+  const lastSave = await state.fetch(LAST_SAVE_ID);
+
+  let defaultName;
+  if (lastSave === undefined) {
+    defaultName = 'new-workspace';
+  } else {
+    defaultName = (lastSave as string)
+      .split('/')
+      .pop()
+      ?.split('.')[0];
+  }
+
+  const defaultPath = browser.model.path + '/' + defaultName + WORKSPACE_EXT;
+
+  return getSavePath(defaultPath).then(async userPath => {
+    if (userPath !== null) {
+      await save(userPath, contents, data, state);
+    }
+  });
+}
+
+/**
+ * Initialization data for the workspace-ui extension.
+ */
+export namespace WorkspaceUI {
+  export function activate(
+    app: JupyterFrontEnd,
+    menu: IMainMenu,
+    fbf: IFileBrowserFactory,
+    resolver: IWindowResolver,
+    state: IStateDB,
+    router: IRouter
+  ) {
+    const ft: DocumentRegistry.IFileType = {
+      name: WORKSPACE_NAME,
+      contentType: 'file',
+      fileFormat: 'text',
+      displayName: 'JupyterLab workspace File',
+      extensions: [WORKSPACE_EXT],
+      mimeTypes: ['text/json'],
+      iconClass: ICON_NAME
+    };
+    app.docRegistry.addFileType(ft);
+
+    // The workspace factory create dummy widgets for the purpose to load the new workspace.
+    const factory = new WorkspaceFactory(
+      app.serviceManager.workspaces,
+      router,
+      state
+    );
+    app.docRegistry.addWidgetFactory(factory);
+
+    app.commands.addCommand(CommandIDs.saveWorkspaceAsCommandId, {
+      label: 'Save Current Workspace As...',
+      execute: async () => {
+        const data = app.serviceManager.workspaces.fetch(resolver.name);
+        await saveAs(
+          fbf.defaultBrowser,
+          app.serviceManager.contents,
+          data,
+          state
+        );
+      }
+    });
+
+    app.commands.addCommand(CommandIDs.saveWorkspaceCommandId, {
+      label: 'Save Current Workspace',
+      execute: async () => {
+        const data = app.serviceManager.workspaces.fetch(resolver.name);
+        const lastSave = await state.fetch('workspace-ui:lastSave');
+        if (lastSave === undefined) {
+          await saveAs(
+            fbf.defaultBrowser,
+            app.serviceManager.contents,
+            data,
+            state
+          );
+        } else {
+          await save(
+            lastSave as string,
+            app.serviceManager.contents,
+            data,
+            state
+          );
+        }
+      }
+    });
+
+    menu.fileMenu.addGroup(
+      [
+        {
+          command: CommandIDs.saveWorkspaceAsCommandId
+        },
+        {
+          command: CommandIDs.saveWorkspaceCommandId
+        }
+      ],
+      40
+    );
+  }
+}

+ 3 - 0
packages/apputils-extension/tsconfig.json

@@ -15,6 +15,9 @@
     {
       "path": "../coreutils"
     },
+    {
+      "path": "../filebrowser"
+    },
     {
       "path": "../mainmenu"
     },