@jupyterlab/workspaces-extension 4.2.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands.d.ts +7 -0
- package/lib/commands.js +475 -0
- package/lib/commands.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +40 -0
- package/lib/index.js.map +1 -0
- package/lib/sidebar.d.ts +5 -0
- package/lib/sidebar.js +75 -0
- package/lib/sidebar.js.map +1 -0
- package/package.json +62 -0
- package/schema/menu.json +70 -0
- package/schema/sidebar.json +51 -0
- package/src/commands.ts +597 -0
- package/src/index.ts +58 -0
- package/src/sidebar.ts +106 -0
- package/style/index.css +11 -0
- package/style/index.js +11 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
IRouter,
|
|
6
|
+
JupyterFrontEnd,
|
|
7
|
+
JupyterFrontEndPlugin
|
|
8
|
+
} from '@jupyterlab/application';
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
InputDialog,
|
|
12
|
+
IWindowResolver,
|
|
13
|
+
showDialog
|
|
14
|
+
} from '@jupyterlab/apputils';
|
|
15
|
+
|
|
16
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
17
|
+
|
|
18
|
+
import { FileDialog, IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
19
|
+
import { Contents, Workspace } from '@jupyterlab/services';
|
|
20
|
+
import { IStateDB } from '@jupyterlab/statedb';
|
|
21
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
22
|
+
import { ICommandPalette } from '@jupyterlab/apputils';
|
|
23
|
+
import { IWorkspaceCommands, IWorkspacesModel } from '@jupyterlab/workspaces';
|
|
24
|
+
|
|
25
|
+
namespace CommandIDs {
|
|
26
|
+
/**
|
|
27
|
+
* Open a workspace by identifier. When no identifier is supplied, falls back to a simple choice dialog.
|
|
28
|
+
*/
|
|
29
|
+
export const open = 'workspace-ui:open';
|
|
30
|
+
/**
|
|
31
|
+
* Save the current workspace.
|
|
32
|
+
*/
|
|
33
|
+
export const save = 'workspace-ui:save';
|
|
34
|
+
/**
|
|
35
|
+
* Save the current workspace under a different path.
|
|
36
|
+
*/
|
|
37
|
+
export const saveAs = 'workspace-ui:save-as';
|
|
38
|
+
/**
|
|
39
|
+
* Create a new workspace.
|
|
40
|
+
*/
|
|
41
|
+
export const createNew = 'workspace-ui:create-new';
|
|
42
|
+
/**
|
|
43
|
+
* Delete a workspace.
|
|
44
|
+
*/
|
|
45
|
+
export const deleteWorkspace = 'workspace-ui:delete';
|
|
46
|
+
/**
|
|
47
|
+
* Clone a given workspace.
|
|
48
|
+
*/
|
|
49
|
+
export const clone = 'workspace-ui:clone';
|
|
50
|
+
/**
|
|
51
|
+
* Rename a workspace.
|
|
52
|
+
*/
|
|
53
|
+
export const rename = 'workspace-ui:rename';
|
|
54
|
+
/**
|
|
55
|
+
* Reset a workspace.
|
|
56
|
+
*/
|
|
57
|
+
export const reset = 'workspace-ui:reset';
|
|
58
|
+
/**
|
|
59
|
+
* Import a workspace.
|
|
60
|
+
*/
|
|
61
|
+
export const importWorkspace = 'workspace-ui:import';
|
|
62
|
+
/**
|
|
63
|
+
* Export a workspace.
|
|
64
|
+
*/
|
|
65
|
+
export const exportWorkspace = 'workspace-ui:export';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const WORKSPACE_NAME = 'jupyterlab-workspace';
|
|
69
|
+
const WORKSPACE_EXT = '.' + WORKSPACE_NAME;
|
|
70
|
+
const LAST_SAVE_ID = 'workspace-ui:lastSave';
|
|
71
|
+
|
|
72
|
+
export const WORKSPACE_ITEM_CLASS = 'jp-mod-workspace';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The workspace commands
|
|
76
|
+
*/
|
|
77
|
+
export const commandsPlugin: JupyterFrontEndPlugin<IWorkspaceCommands> = {
|
|
78
|
+
id: '@jupyterlab/workspaces:commands',
|
|
79
|
+
description: 'Add workspace commands.',
|
|
80
|
+
autoStart: true,
|
|
81
|
+
requires: [
|
|
82
|
+
IWorkspacesModel,
|
|
83
|
+
IDefaultFileBrowser,
|
|
84
|
+
IWindowResolver,
|
|
85
|
+
IStateDB,
|
|
86
|
+
ITranslator,
|
|
87
|
+
JupyterFrontEnd.IPaths
|
|
88
|
+
],
|
|
89
|
+
provides: IWorkspaceCommands,
|
|
90
|
+
optional: [IRouter, ICommandPalette],
|
|
91
|
+
activate: (
|
|
92
|
+
app: JupyterFrontEnd,
|
|
93
|
+
model: IWorkspacesModel,
|
|
94
|
+
fileBrowser: IDefaultFileBrowser,
|
|
95
|
+
resolver: IWindowResolver,
|
|
96
|
+
state: IStateDB,
|
|
97
|
+
translator: ITranslator,
|
|
98
|
+
paths: JupyterFrontEnd.IPaths,
|
|
99
|
+
router: IRouter | null,
|
|
100
|
+
palette: ICommandPalette | null
|
|
101
|
+
): IWorkspaceCommands => {
|
|
102
|
+
const trans = translator.load('jupyterlab');
|
|
103
|
+
|
|
104
|
+
const namingHintLabel = trans.__(
|
|
105
|
+
'Naming the workspace will create a unique URL. The name may contain letters, numbers, hyphens (-), and underscores (_).'
|
|
106
|
+
);
|
|
107
|
+
const workspacesBase = URLExt.join(paths.urls.app, 'workspaces');
|
|
108
|
+
const resourcePrefix = workspacesBase + '/';
|
|
109
|
+
// `-` needs double backslashes to be escaped in `v` mode used for patterns
|
|
110
|
+
const namePattern = '[a-zA-Z0-9\\-_]+';
|
|
111
|
+
|
|
112
|
+
const getNameForWorkspace = async (
|
|
113
|
+
options: Omit<InputDialog.ITextOptions, 'prefix' | 'pattern' | 'label'>
|
|
114
|
+
) => {
|
|
115
|
+
return InputDialog.getText({
|
|
116
|
+
label: namingHintLabel,
|
|
117
|
+
prefix: resourcePrefix,
|
|
118
|
+
pattern: namePattern,
|
|
119
|
+
required: true,
|
|
120
|
+
placeholder: trans.__('workspace-name'),
|
|
121
|
+
...options
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const test = (node: HTMLElement) =>
|
|
126
|
+
node.classList.contains(WORKSPACE_ITEM_CLASS);
|
|
127
|
+
|
|
128
|
+
app.commands.addCommand(CommandIDs.open, {
|
|
129
|
+
label: args => {
|
|
130
|
+
const workspaceId = args.workspace as string | undefined;
|
|
131
|
+
return workspaceId
|
|
132
|
+
? trans.__('Open Workspace')
|
|
133
|
+
: trans.__('Open Workspace…');
|
|
134
|
+
},
|
|
135
|
+
execute: async args => {
|
|
136
|
+
let workspaceId = args.workspace as string | undefined;
|
|
137
|
+
|
|
138
|
+
if (!workspaceId) {
|
|
139
|
+
const result = await InputDialog.getItem({
|
|
140
|
+
title: trans.__('Choose Workspace To Open'),
|
|
141
|
+
label: trans.__('Choose an existing workspace to open.'),
|
|
142
|
+
items: model.identifiers,
|
|
143
|
+
okLabel: trans.__('Choose'),
|
|
144
|
+
prefix: resourcePrefix
|
|
145
|
+
});
|
|
146
|
+
if (!result.value || !result.button.accept) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
workspaceId = result.value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!workspaceId) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const url = URLExt.join(workspacesBase, workspaceId);
|
|
157
|
+
if (!url.startsWith(workspacesBase)) {
|
|
158
|
+
throw new Error('Can only be used for workspaces');
|
|
159
|
+
}
|
|
160
|
+
if (router) {
|
|
161
|
+
router.navigate(url, { hard: true });
|
|
162
|
+
} else {
|
|
163
|
+
document.location.href = url;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
app.commands.addCommand(CommandIDs.deleteWorkspace, {
|
|
169
|
+
label: trans.__('Delete Workspace…'),
|
|
170
|
+
execute: async args => {
|
|
171
|
+
const node = app.contextMenuHitTest(test);
|
|
172
|
+
let workspaceId =
|
|
173
|
+
(args.workspace as string | undefined) ?? node?.dataset['context'];
|
|
174
|
+
|
|
175
|
+
if (!workspaceId) {
|
|
176
|
+
const result = await InputDialog.getItem({
|
|
177
|
+
title: trans.__('Choose Workspace To Delete'),
|
|
178
|
+
label: trans.__('Choose an existing workspace to delete.'),
|
|
179
|
+
items: model.identifiers,
|
|
180
|
+
okLabel: trans.__('Choose')
|
|
181
|
+
});
|
|
182
|
+
if (!result.value || !result.button.accept) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
workspaceId = result.value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!workspaceId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const result = await showDialog({
|
|
192
|
+
title: trans.__('Delete workspace'),
|
|
193
|
+
body: trans.__(
|
|
194
|
+
'Deleting workspace "%1" will also delete its URL. A deleted workspace cannot be recovered.',
|
|
195
|
+
workspaceId
|
|
196
|
+
),
|
|
197
|
+
buttons: [
|
|
198
|
+
Dialog.cancelButton(),
|
|
199
|
+
Dialog.warnButton({ label: trans.__('Delete') })
|
|
200
|
+
],
|
|
201
|
+
defaultButton: 0
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (result.button.accept) {
|
|
205
|
+
await model.remove(workspaceId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
app.commands.addCommand(CommandIDs.createNew, {
|
|
211
|
+
label: trans.__('Create New Workspace…'),
|
|
212
|
+
execute: async args => {
|
|
213
|
+
let workspaceId = args.workspace as string | undefined;
|
|
214
|
+
|
|
215
|
+
if (!workspaceId) {
|
|
216
|
+
const result = await getNameForWorkspace({
|
|
217
|
+
title: trans.__('Create New Workspace'),
|
|
218
|
+
okLabel: trans.__('Create')
|
|
219
|
+
});
|
|
220
|
+
if (!result.value || !result.button.accept) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
workspaceId = result.value;
|
|
224
|
+
}
|
|
225
|
+
if (!workspaceId) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await model.create(workspaceId);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
app.commands.addCommand(CommandIDs.clone, {
|
|
233
|
+
label: trans.__('Clone Workspace…'),
|
|
234
|
+
execute: async args => {
|
|
235
|
+
const node = app.contextMenuHitTest(test);
|
|
236
|
+
let workspaceId =
|
|
237
|
+
(args.workspace as string | undefined) ?? node?.dataset['context'];
|
|
238
|
+
if (!workspaceId) {
|
|
239
|
+
const result = await InputDialog.getItem({
|
|
240
|
+
title: trans.__('Choose Workspace To Clone'),
|
|
241
|
+
label: trans.__('Choose an existing workspace to clone.'),
|
|
242
|
+
items: model.identifiers,
|
|
243
|
+
okLabel: trans.__('Choose')
|
|
244
|
+
});
|
|
245
|
+
if (!result.value || !result.button.accept) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
workspaceId = result.value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const result = await getNameForWorkspace({
|
|
252
|
+
title: trans.__('Clone Workspace'),
|
|
253
|
+
text: trans.__('%1-clone', workspaceId),
|
|
254
|
+
okLabel: trans.__('Clone')
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!result.button.accept || !result.value) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let newName = result.value;
|
|
262
|
+
|
|
263
|
+
await model.saveAs(workspaceId, newName);
|
|
264
|
+
|
|
265
|
+
if (workspaceId === resolver.name) {
|
|
266
|
+
// If the current workspace was cloned, open the cloned copy
|
|
267
|
+
return app.commands.execute(CommandIDs.open, { workspace: newName });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
app.commands.addCommand(CommandIDs.rename, {
|
|
273
|
+
label: trans.__('Rename Workspace…'),
|
|
274
|
+
execute: async args => {
|
|
275
|
+
const node = app.contextMenuHitTest(test);
|
|
276
|
+
const workspaceId =
|
|
277
|
+
(args.workspace as string | undefined) ??
|
|
278
|
+
node?.dataset['context'] ??
|
|
279
|
+
resolver.name;
|
|
280
|
+
|
|
281
|
+
const oldName = workspaceId;
|
|
282
|
+
const result = await getNameForWorkspace({
|
|
283
|
+
title: trans.__('Rename Workspace'),
|
|
284
|
+
text: oldName,
|
|
285
|
+
okLabel: trans.__('Rename')
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!result.button.accept || !result.value) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let newName = result.value;
|
|
293
|
+
|
|
294
|
+
await model.rename(workspaceId, newName);
|
|
295
|
+
|
|
296
|
+
if (workspaceId === resolver.name) {
|
|
297
|
+
// If the current workspace was renamed, reopen it to ensure consistent state
|
|
298
|
+
return app.commands.execute(CommandIDs.open, { workspace: newName });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
app.commands.addCommand(CommandIDs.reset, {
|
|
304
|
+
label: trans.__('Reset Workspace…'),
|
|
305
|
+
execute: async args => {
|
|
306
|
+
const node = app.contextMenuHitTest(test);
|
|
307
|
+
const workspaceId =
|
|
308
|
+
(args.workspace as string | undefined) ??
|
|
309
|
+
node?.dataset['context'] ??
|
|
310
|
+
resolver.name;
|
|
311
|
+
|
|
312
|
+
const workspace =
|
|
313
|
+
await app.serviceManager.workspaces.fetch(workspaceId);
|
|
314
|
+
const tabs = (workspace.data['layout-restorer:data'] as any)?.main?.dock
|
|
315
|
+
?.widgets?.length;
|
|
316
|
+
|
|
317
|
+
const result = await showDialog({
|
|
318
|
+
title: trans.__('Reset Workspace'),
|
|
319
|
+
body: trans._n(
|
|
320
|
+
'Resetting workspace %2 will close its %1 tab and return to default layout.',
|
|
321
|
+
'Resetting workspace %2 will close its %1 tabs and return to default layout.',
|
|
322
|
+
tabs,
|
|
323
|
+
workspaceId
|
|
324
|
+
),
|
|
325
|
+
buttons: [
|
|
326
|
+
Dialog.cancelButton(),
|
|
327
|
+
Dialog.warnButton({ label: trans.__('Reset') })
|
|
328
|
+
],
|
|
329
|
+
defaultButton: 0
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!result.button.accept) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await model.reset(workspaceId);
|
|
337
|
+
|
|
338
|
+
if (workspaceId === resolver.name) {
|
|
339
|
+
// If the current workspace was reset, refresh it to take effect
|
|
340
|
+
return app.commands.execute(CommandIDs.open, {
|
|
341
|
+
workspace: workspaceId
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
await model.refresh();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Commands persisting (or reading from) Jupyter file system
|
|
351
|
+
*/
|
|
352
|
+
|
|
353
|
+
app.commands.addCommand(CommandIDs.importWorkspace, {
|
|
354
|
+
label: trans.__('Import Workspace…'),
|
|
355
|
+
execute: async () => {
|
|
356
|
+
const { contents } = app.serviceManager;
|
|
357
|
+
const result = await FileDialog.getOpenFiles({
|
|
358
|
+
manager: fileBrowser.model.manager,
|
|
359
|
+
title: trans.__('Select Workspace Files to Import'),
|
|
360
|
+
filter: (model: Contents.IModel) =>
|
|
361
|
+
model.type === 'directory' || model.path.endsWith(WORKSPACE_EXT)
|
|
362
|
+
? {}
|
|
363
|
+
: null,
|
|
364
|
+
label: trans.__(
|
|
365
|
+
'You choose one or more workspace files to import. A Jupyter Workspace File has %1 extension.',
|
|
366
|
+
WORKSPACE_EXT
|
|
367
|
+
),
|
|
368
|
+
translator
|
|
369
|
+
});
|
|
370
|
+
if (result.button.accept && result.value && result.value.length >= 1) {
|
|
371
|
+
for (const fileModel of result.value) {
|
|
372
|
+
// Get the file contents too
|
|
373
|
+
const fullFile = await contents.get(fileModel.path, {
|
|
374
|
+
content: true
|
|
375
|
+
});
|
|
376
|
+
const workspace = JSON.parse(
|
|
377
|
+
fullFile.content
|
|
378
|
+
) as unknown as Workspace.IWorkspace;
|
|
379
|
+
await app.serviceManager.workspaces.save(
|
|
380
|
+
workspace.metadata.id,
|
|
381
|
+
workspace
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
await model.refresh();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
app.commands.addCommand(CommandIDs.exportWorkspace, {
|
|
390
|
+
label: trans.__('Export Workspace…'),
|
|
391
|
+
execute: async args => {
|
|
392
|
+
const { contents } = app.serviceManager;
|
|
393
|
+
|
|
394
|
+
const node = app.contextMenuHitTest(test);
|
|
395
|
+
let workspaceId =
|
|
396
|
+
(args.workspace as string | undefined) ??
|
|
397
|
+
node?.dataset['context'] ??
|
|
398
|
+
resolver.name;
|
|
399
|
+
|
|
400
|
+
if (!workspaceId) {
|
|
401
|
+
// When invoked from main menu or command palette we need to ask which workspace to export
|
|
402
|
+
const result = await InputDialog.getItem({
|
|
403
|
+
title: trans.__('Choose Workspace To Export'),
|
|
404
|
+
label: trans.__('Choose an existing workspace to export.'),
|
|
405
|
+
items: model.identifiers,
|
|
406
|
+
okLabel: trans.__('Choose')
|
|
407
|
+
});
|
|
408
|
+
if (!result.value || !result.button.accept) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
workspaceId = result.value;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const data = app.serviceManager.workspaces.fetch(workspaceId);
|
|
415
|
+
|
|
416
|
+
const result = await FileDialog.getExistingDirectory({
|
|
417
|
+
title: trans.__('Choose Workspace Export Directory'),
|
|
418
|
+
defaultPath: fileBrowser.model.path,
|
|
419
|
+
manager: fileBrowser.model.manager,
|
|
420
|
+
label: trans.__(
|
|
421
|
+
'The "%1" workspace will be saved in the chosen directory as "%1%2".',
|
|
422
|
+
workspaceId,
|
|
423
|
+
WORKSPACE_EXT
|
|
424
|
+
),
|
|
425
|
+
translator
|
|
426
|
+
});
|
|
427
|
+
if (
|
|
428
|
+
!result.button.accept ||
|
|
429
|
+
!result.value ||
|
|
430
|
+
result.value.length === 0
|
|
431
|
+
) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (result.value.length > 1) {
|
|
435
|
+
console.warn(
|
|
436
|
+
'More than one directory was selected; the workspace will be exported to the first directory only'
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const exportPath =
|
|
440
|
+
result.value[0].path + '/' + workspaceId + WORKSPACE_EXT;
|
|
441
|
+
|
|
442
|
+
if (exportPath) {
|
|
443
|
+
await Private.save(exportPath, contents, data, state, false);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
app.commands.addCommand(CommandIDs.saveAs, {
|
|
449
|
+
label: trans.__('Save Current Workspace As…'),
|
|
450
|
+
execute: async () => {
|
|
451
|
+
const { contents } = app.serviceManager;
|
|
452
|
+
const data = app.serviceManager.workspaces.fetch(resolver.name);
|
|
453
|
+
await Private.saveAs(fileBrowser, contents, data, state, translator);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
app.commands.addCommand(CommandIDs.save, {
|
|
458
|
+
label: trans.__('Save Current Workspace…'),
|
|
459
|
+
execute: async () => {
|
|
460
|
+
const { contents } = app.serviceManager;
|
|
461
|
+
const data = app.serviceManager.workspaces.fetch(resolver.name);
|
|
462
|
+
const lastSave = (await state.fetch(LAST_SAVE_ID)) as string;
|
|
463
|
+
if (lastSave === undefined) {
|
|
464
|
+
await Private.saveAs(fileBrowser, contents, data, state, translator);
|
|
465
|
+
} else {
|
|
466
|
+
await Private.save(lastSave, contents, data, state);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (palette) {
|
|
472
|
+
const category = trans.__('Workspaces');
|
|
473
|
+
const commands = [
|
|
474
|
+
CommandIDs.open,
|
|
475
|
+
CommandIDs.save,
|
|
476
|
+
CommandIDs.saveAs,
|
|
477
|
+
CommandIDs.createNew,
|
|
478
|
+
CommandIDs.rename,
|
|
479
|
+
CommandIDs.clone,
|
|
480
|
+
CommandIDs.exportWorkspace,
|
|
481
|
+
CommandIDs.importWorkspace,
|
|
482
|
+
CommandIDs.reset,
|
|
483
|
+
CommandIDs.deleteWorkspace
|
|
484
|
+
];
|
|
485
|
+
for (const command of commands) {
|
|
486
|
+
palette.addItem({
|
|
487
|
+
command,
|
|
488
|
+
category
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
open: CommandIDs.open,
|
|
495
|
+
deleteWorkspace: CommandIDs.deleteWorkspace
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
namespace Private {
|
|
501
|
+
export function createNameFromPath(path: string): string {
|
|
502
|
+
let name = path.split('/').pop();
|
|
503
|
+
if (name === undefined) {
|
|
504
|
+
return 'unnamed-workspace';
|
|
505
|
+
}
|
|
506
|
+
// Remove the workspace suffix if present
|
|
507
|
+
if (name.endsWith(WORKSPACE_EXT)) {
|
|
508
|
+
name = name.slice(0, -WORKSPACE_EXT.length);
|
|
509
|
+
}
|
|
510
|
+
return name;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Save workspace to a user provided location
|
|
515
|
+
*/
|
|
516
|
+
export async function save(
|
|
517
|
+
userPath: string,
|
|
518
|
+
contents: Contents.IManager,
|
|
519
|
+
data: Promise<Workspace.IWorkspace>,
|
|
520
|
+
state: IStateDB,
|
|
521
|
+
rememberAsLastSave = true
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
const name = createNameFromPath(userPath);
|
|
524
|
+
|
|
525
|
+
// Add extension if not provided
|
|
526
|
+
if (!userPath.endsWith(WORKSPACE_EXT)) {
|
|
527
|
+
userPath = userPath + WORKSPACE_EXT;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (rememberAsLastSave) {
|
|
531
|
+
// Save last save location, for save button to work
|
|
532
|
+
await state.save(LAST_SAVE_ID, userPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const resolvedData = await data;
|
|
536
|
+
resolvedData.metadata.id = `${name}`;
|
|
537
|
+
await contents.save(userPath, {
|
|
538
|
+
type: 'file',
|
|
539
|
+
format: 'text',
|
|
540
|
+
content: JSON.stringify(resolvedData)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Ask user for location, and save workspace.
|
|
546
|
+
* Default location is the current directory in the file browser
|
|
547
|
+
*/
|
|
548
|
+
export async function saveAs(
|
|
549
|
+
browser: IDefaultFileBrowser,
|
|
550
|
+
contents: Contents.IManager,
|
|
551
|
+
data: Promise<Workspace.IWorkspace>,
|
|
552
|
+
state: IStateDB,
|
|
553
|
+
translator?: ITranslator
|
|
554
|
+
): Promise<void> {
|
|
555
|
+
translator = translator || nullTranslator;
|
|
556
|
+
const lastSave = await state.fetch(LAST_SAVE_ID);
|
|
557
|
+
|
|
558
|
+
let defaultName;
|
|
559
|
+
if (lastSave === undefined) {
|
|
560
|
+
defaultName = 'new-workspace';
|
|
561
|
+
} else {
|
|
562
|
+
defaultName = (lastSave as string).split('/').pop()?.split('.')[0];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const defaultPath = browser.model.path + '/' + defaultName + WORKSPACE_EXT;
|
|
566
|
+
const userPath = await getSavePath(defaultPath, translator);
|
|
567
|
+
|
|
568
|
+
if (userPath) {
|
|
569
|
+
await save(userPath, contents, data, state);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Ask user for a path to save to.
|
|
575
|
+
* @param defaultPath Path already present when the dialog is shown
|
|
576
|
+
*/
|
|
577
|
+
async function getSavePath(
|
|
578
|
+
defaultPath: string,
|
|
579
|
+
translator?: ITranslator
|
|
580
|
+
): Promise<string | null> {
|
|
581
|
+
translator = translator || nullTranslator;
|
|
582
|
+
const trans = translator.load('jupyterlab');
|
|
583
|
+
const result = await InputDialog.getText({
|
|
584
|
+
title: trans.__('Save Current Workspace As…'),
|
|
585
|
+
text: defaultPath,
|
|
586
|
+
placeholder: trans.__('Path to save the workspace in'),
|
|
587
|
+
okLabel: trans.__('Save'),
|
|
588
|
+
// select the path without extension
|
|
589
|
+
selectionRange: defaultPath.length - WORKSPACE_EXT.length
|
|
590
|
+
});
|
|
591
|
+
if (result.button.accept) {
|
|
592
|
+
return result.value;
|
|
593
|
+
} else {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
/**
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
* @module workspaces-extension
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
JupyterFrontEnd,
|
|
11
|
+
JupyterFrontEndPlugin
|
|
12
|
+
} from '@jupyterlab/application';
|
|
13
|
+
import {
|
|
14
|
+
IWorkspaceCommands,
|
|
15
|
+
IWorkspacesModel,
|
|
16
|
+
WorkspacesModel
|
|
17
|
+
} from '@jupyterlab/workspaces';
|
|
18
|
+
import { commandsPlugin } from './commands';
|
|
19
|
+
import { workspacesSidebar } from './sidebar';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The extension populating sidebar with workspaces list.
|
|
23
|
+
*/
|
|
24
|
+
const workspacesModel: JupyterFrontEndPlugin<IWorkspacesModel> = {
|
|
25
|
+
id: '@jupyterlab/workspaces-extension:model',
|
|
26
|
+
description: 'Provides a model for available workspaces.',
|
|
27
|
+
provides: IWorkspacesModel,
|
|
28
|
+
autoStart: true,
|
|
29
|
+
activate: (app: JupyterFrontEnd) => {
|
|
30
|
+
return new WorkspacesModel({
|
|
31
|
+
manager: app.serviceManager.workspaces
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The extension providing workspace sub-menu in the "File" main menu.
|
|
38
|
+
*/
|
|
39
|
+
const workspacesMenu: JupyterFrontEndPlugin<void> = {
|
|
40
|
+
id: '@jupyterlab/workspaces-extension:menu',
|
|
41
|
+
description: 'Populates "File" main menu with Workspaces submenu.',
|
|
42
|
+
requires: [IWorkspaceCommands],
|
|
43
|
+
autoStart: true,
|
|
44
|
+
activate: () => {
|
|
45
|
+
// no-op - the menu items come from schema matching the name of the plugin
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Export the plugins as default.
|
|
51
|
+
*/
|
|
52
|
+
const plugins: JupyterFrontEndPlugin<any>[] = [
|
|
53
|
+
workspacesModel,
|
|
54
|
+
commandsPlugin,
|
|
55
|
+
workspacesSidebar,
|
|
56
|
+
workspacesMenu
|
|
57
|
+
];
|
|
58
|
+
export default plugins;
|