@fails-components/jupyter-filesystem-extension 0.0.1-alpha.10
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/LICENSE +29 -0
- package/README.md +102 -0
- package/lib/contents.d.ts +31 -0
- package/lib/contents.js +165 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +37 -0
- package/lib/settings.d.ts +15 -0
- package/lib/settings.js +79 -0
- package/package.json +211 -0
- package/src/contents.ts +218 -0
- package/src/index.ts +45 -0
- package/src/settings.ts +100 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Marten Richter
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# fails_components_jupyter_filesystem_extension
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fails-components/jupyterfails/actions/workflows/build.yml)
|
|
4
|
+
|
|
5
|
+
This is an extension, that taps into jupyter lite's filesystem handling to be able to remotely control and supply the files visible to jupyter lite.
|
|
6
|
+
It is probably only useful together with fails-components' jupyter launcher plugin,
|
|
7
|
+
that allows to control jupyter lite embedded inside an iframe (build from the jupyterfails' repos configs).
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- JupyterLab >= 4.0.0
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
To install the extension, execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install fails_components_jupyter_filesystem_extension
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Uninstall
|
|
22
|
+
|
|
23
|
+
To remove the extension, execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip uninstall fails_components_jupyter_filesystem_extension
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Contributing
|
|
30
|
+
|
|
31
|
+
### Development install
|
|
32
|
+
|
|
33
|
+
Note: You will need NodeJS to build the extension package.
|
|
34
|
+
|
|
35
|
+
The `jlpm` command is JupyterLab's pinned version of
|
|
36
|
+
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
|
|
37
|
+
`yarn` or `npm` in lieu of `jlpm` below.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Clone the repo to your local environment
|
|
41
|
+
# Change directory to the fails_components_jupyter_filesystem_extension directory
|
|
42
|
+
jlpm build
|
|
43
|
+
# Install package in development mode
|
|
44
|
+
pip install -e "."
|
|
45
|
+
# Link your development version of the extension with JupyterLab
|
|
46
|
+
jupyter labextension develop . --overwrite
|
|
47
|
+
# Rebuild extension Typescript source after making changes
|
|
48
|
+
jlpm build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
55
|
+
jlpm watch
|
|
56
|
+
# Run JupyterLab in another terminal
|
|
57
|
+
jupyter lab
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
|
|
61
|
+
|
|
62
|
+
By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
jupyter lab build --minimize=False
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Development uninstall
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip uninstall fails_components_jupyter_filesystem_extension
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
|
|
75
|
+
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
|
|
76
|
+
folder is located. Then you can remove the symlink named `@fails-components/jupyter-applet-view` within that folder.
|
|
77
|
+
|
|
78
|
+
### Testing the extension
|
|
79
|
+
|
|
80
|
+
#### Frontend tests
|
|
81
|
+
|
|
82
|
+
This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
|
|
83
|
+
|
|
84
|
+
To execute them, execute:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
jlpm
|
|
88
|
+
jlpm test
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
<!--
|
|
92
|
+
#### Integration tests
|
|
93
|
+
|
|
94
|
+
This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
|
|
95
|
+
More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
|
|
96
|
+
|
|
97
|
+
More information are provided within the [ui-tests](./ui-tests/README.md) README.
|
|
98
|
+
|
|
99
|
+
### Packaging the extension
|
|
100
|
+
|
|
101
|
+
See [RELEASE](RELEASE.md)
|
|
102
|
+
-->
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Contents as ServerContents } from '@jupyterlab/services';
|
|
2
|
+
import { IContents } from '@jupyterlite/contents';
|
|
3
|
+
import { IContentEventType } from '@fails-components/jupyter-launcher';
|
|
4
|
+
export declare class FailsContents implements IContents {
|
|
5
|
+
constructor();
|
|
6
|
+
onMessage(event: IContentEventType): Promise<any>;
|
|
7
|
+
get ready(): Promise<void>;
|
|
8
|
+
initialize(): Promise<void>;
|
|
9
|
+
get(path: string, options?: ServerContents.IFetchOptions): Promise<ServerContents.IModel | null>;
|
|
10
|
+
save(path: string, options?: Partial<ServerContents.IModel>): Promise<ServerContents.IModel | null>;
|
|
11
|
+
newUntitled(options?: ServerContents.ICreateOptions): Promise<ServerContents.IModel | null>;
|
|
12
|
+
rename(oldLocalPath: string, newLocalPath: string): Promise<ServerContents.IModel>;
|
|
13
|
+
delete(path: string): Promise<void>;
|
|
14
|
+
copy(path: string, toDir: string): Promise<ServerContents.IModel>;
|
|
15
|
+
createCheckpoint(path: string): Promise<ServerContents.ICheckpointModel>;
|
|
16
|
+
listCheckpoints(path: string): Promise<ServerContents.ICheckpointModel[]>;
|
|
17
|
+
restoreCheckpoint(path: string, checkpointID: string): Promise<void>;
|
|
18
|
+
deleteCheckpoint(path: string, checkpointID: string): Promise<void>;
|
|
19
|
+
static EMPTY_NB: {
|
|
20
|
+
metadata: {
|
|
21
|
+
orig_nbformat: number;
|
|
22
|
+
};
|
|
23
|
+
nbformat_minor: number;
|
|
24
|
+
nbformat: number;
|
|
25
|
+
cells: never[];
|
|
26
|
+
};
|
|
27
|
+
private _ready;
|
|
28
|
+
private _fileContent;
|
|
29
|
+
private _fileName;
|
|
30
|
+
private _failsCallbacks;
|
|
31
|
+
}
|
package/lib/contents.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
|
+
// portions used from Jupyterlab:
|
|
3
|
+
/* -----------------------------------------------------------------------------
|
|
4
|
+
| Copyright (c) Jupyter Development Team.
|
|
5
|
+
| Distributed under the terms of the Modified BSD License.
|
|
6
|
+
|----------------------------------------------------------------------------*/
|
|
7
|
+
// This code contains portions from or is inspired by Jupyter lab and lite
|
|
8
|
+
const jsonMime = 'application/json';
|
|
9
|
+
class FailsContents {
|
|
10
|
+
constructor() {
|
|
11
|
+
this._fileContent = JSON.stringify(FailsContents.EMPTY_NB);
|
|
12
|
+
this._fileName = 'unloaded.ipynb';
|
|
13
|
+
this._ready = new PromiseDelegate();
|
|
14
|
+
if (!window.failsCallbacks) {
|
|
15
|
+
window.failsCallbacks = {};
|
|
16
|
+
}
|
|
17
|
+
this._failsCallbacks = window.failsCallbacks;
|
|
18
|
+
this._failsCallbacks.callContents = this.onMessage.bind(this);
|
|
19
|
+
}
|
|
20
|
+
async onMessage(event) {
|
|
21
|
+
// todo handle events
|
|
22
|
+
switch (event.task) {
|
|
23
|
+
case 'loadFile':
|
|
24
|
+
{
|
|
25
|
+
const loadevent = event;
|
|
26
|
+
this._fileContent = JSON.stringify(loadevent.fileData || FailsContents.EMPTY_NB);
|
|
27
|
+
this._fileName = loadevent.fileName;
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case 'savedFile':
|
|
31
|
+
{
|
|
32
|
+
const savedevent = event;
|
|
33
|
+
if (this._fileName !== savedevent.fileName) {
|
|
34
|
+
return { error: 'Filename not found' };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
fileData: JSON.parse(this._fileContent)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
get ready() {
|
|
44
|
+
return this._ready.promise;
|
|
45
|
+
}
|
|
46
|
+
async initialize() {
|
|
47
|
+
this._ready.resolve(void 0);
|
|
48
|
+
}
|
|
49
|
+
async get(path, options) {
|
|
50
|
+
// remove leading slash
|
|
51
|
+
path = decodeURIComponent(path.replace(/^\//, ''));
|
|
52
|
+
const serverFile = {
|
|
53
|
+
name: this._fileName,
|
|
54
|
+
path: this._fileName,
|
|
55
|
+
last_modified: new Date(0).toISOString(),
|
|
56
|
+
created: new Date(0).toISOString(),
|
|
57
|
+
format: 'json',
|
|
58
|
+
mimetype: jsonMime,
|
|
59
|
+
content: JSON.parse(this._fileContent),
|
|
60
|
+
size: 0,
|
|
61
|
+
writable: true,
|
|
62
|
+
type: 'notebook'
|
|
63
|
+
};
|
|
64
|
+
if (path === '') {
|
|
65
|
+
// the local directory, return the info about the proxy notebook
|
|
66
|
+
return {
|
|
67
|
+
name: '',
|
|
68
|
+
path,
|
|
69
|
+
last_modified: new Date(0).toISOString(),
|
|
70
|
+
created: new Date(0).toISOString(),
|
|
71
|
+
format: 'json',
|
|
72
|
+
mimetype: jsonMime,
|
|
73
|
+
content: [serverFile],
|
|
74
|
+
size: 0,
|
|
75
|
+
writable: true,
|
|
76
|
+
type: 'directory'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (path === this._fileName) {
|
|
80
|
+
return serverFile;
|
|
81
|
+
}
|
|
82
|
+
return null; // not found
|
|
83
|
+
}
|
|
84
|
+
async save(path, options = {}) {
|
|
85
|
+
path = decodeURIComponent(path);
|
|
86
|
+
if (path !== this._fileName) {
|
|
87
|
+
// we only allow the proxy object
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const chunk = options.chunk;
|
|
91
|
+
const chunked = chunk ? chunk > 1 || chunk === -1 : false;
|
|
92
|
+
let item = await this.get(path, {
|
|
93
|
+
content: chunked
|
|
94
|
+
});
|
|
95
|
+
if (!item) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const modified = new Date().toISOString();
|
|
99
|
+
// override with the new values
|
|
100
|
+
item = {
|
|
101
|
+
...item,
|
|
102
|
+
...options,
|
|
103
|
+
last_modified: modified
|
|
104
|
+
};
|
|
105
|
+
if (options.content && options.format === 'base64') {
|
|
106
|
+
const lastChunk = chunk ? chunk === -1 : true;
|
|
107
|
+
const modified = new Date().toISOString();
|
|
108
|
+
// override with the new values
|
|
109
|
+
item = {
|
|
110
|
+
...item,
|
|
111
|
+
...options,
|
|
112
|
+
last_modified: modified
|
|
113
|
+
};
|
|
114
|
+
const originalContent = item.content;
|
|
115
|
+
const escaped = decodeURIComponent(escape(atob(options.content)));
|
|
116
|
+
const newcontent = chunked ? originalContent + escaped : escaped;
|
|
117
|
+
item = {
|
|
118
|
+
...item,
|
|
119
|
+
content: lastChunk ? JSON.parse(newcontent) : newcontent,
|
|
120
|
+
format: 'json',
|
|
121
|
+
type: 'notebook',
|
|
122
|
+
size: newcontent.length
|
|
123
|
+
};
|
|
124
|
+
this._fileContent = JSON.stringify(newcontent); // no parsing
|
|
125
|
+
return item;
|
|
126
|
+
}
|
|
127
|
+
this._fileContent = JSON.stringify(item.content); // no parsing
|
|
128
|
+
return item;
|
|
129
|
+
}
|
|
130
|
+
// For fails creating a new file is not allowed, so no need to implment it
|
|
131
|
+
async newUntitled(options) {
|
|
132
|
+
throw new Error('NewUntitled not implemented');
|
|
133
|
+
}
|
|
134
|
+
async rename(oldLocalPath, newLocalPath) {
|
|
135
|
+
throw new Error('rename not implemented');
|
|
136
|
+
}
|
|
137
|
+
async delete(path) {
|
|
138
|
+
throw new Error('delete not implemented');
|
|
139
|
+
}
|
|
140
|
+
async copy(path, toDir) {
|
|
141
|
+
throw new Error('copy not implemented');
|
|
142
|
+
}
|
|
143
|
+
async createCheckpoint(path) {
|
|
144
|
+
throw new Error('createCheckpoint not (yet?) implemented');
|
|
145
|
+
}
|
|
146
|
+
async listCheckpoints(path) {
|
|
147
|
+
// throw new Error('listCheckpoints not (yet?) implemented');
|
|
148
|
+
return [{ id: 'fakeCheckpoint', last_modified: new Date().toISOString() }];
|
|
149
|
+
}
|
|
150
|
+
async restoreCheckpoint(path, checkpointID) {
|
|
151
|
+
throw new Error('restoreCheckpoint not (yet?) implemented');
|
|
152
|
+
}
|
|
153
|
+
async deleteCheckpoint(path, checkpointID) {
|
|
154
|
+
throw new Error('deleteCheckpoint not (yet?) implemented');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
FailsContents.EMPTY_NB = {
|
|
158
|
+
metadata: {
|
|
159
|
+
orig_nbformat: 4
|
|
160
|
+
},
|
|
161
|
+
nbformat_minor: 5,
|
|
162
|
+
nbformat: 4,
|
|
163
|
+
cells: []
|
|
164
|
+
};
|
|
165
|
+
export { FailsContents };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IContents } from '@jupyterlite/contents';
|
|
2
|
+
import { JupyterLiteServerPlugin } from '@jupyterlite/server';
|
|
3
|
+
export declare const failsContentsPlugin: JupyterLiteServerPlugin<IContents>;
|
|
4
|
+
declare const plugins: JupyterLiteServerPlugin<any>[];
|
|
5
|
+
export default plugins;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IContents } from '@jupyterlite/contents';
|
|
2
|
+
import { ISettings } from '@jupyterlite/settings';
|
|
3
|
+
import { FailsContents } from './contents';
|
|
4
|
+
import { FailsSettings } from './settings';
|
|
5
|
+
export const failsContentsPlugin = {
|
|
6
|
+
id: '@fails-components/jupyter-fails-server:contents',
|
|
7
|
+
requires: [],
|
|
8
|
+
autoStart: true,
|
|
9
|
+
provides: IContents,
|
|
10
|
+
activate: (app) => {
|
|
11
|
+
if (app.namespace !== 'JupyterLite Server') {
|
|
12
|
+
console.log('Not on server');
|
|
13
|
+
}
|
|
14
|
+
const contents = new FailsContents();
|
|
15
|
+
app.started.then(() => contents.initialize().catch(console.warn));
|
|
16
|
+
return contents;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const failsSettingsPlugin = {
|
|
20
|
+
id: '@fails-components/jupyter-fails-server:settings',
|
|
21
|
+
requires: [],
|
|
22
|
+
autoStart: true,
|
|
23
|
+
provides: ISettings,
|
|
24
|
+
activate: (app) => {
|
|
25
|
+
if (app.namespace !== 'JupyterLite Server') {
|
|
26
|
+
console.log('Not on server');
|
|
27
|
+
}
|
|
28
|
+
const settings = new FailsSettings();
|
|
29
|
+
app.started.then(() => settings.initialize().catch(console.warn));
|
|
30
|
+
return settings;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const plugins = [
|
|
34
|
+
failsContentsPlugin,
|
|
35
|
+
failsSettingsPlugin
|
|
36
|
+
];
|
|
37
|
+
export default plugins;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ISettings, IPlugin as ISettingsPlugin } from '@jupyterlite/settings';
|
|
2
|
+
export declare class FailsSettings implements ISettings {
|
|
3
|
+
static _overrides: Record<string, ISettingsPlugin['schema']['default']>;
|
|
4
|
+
static override(plugin: ISettingsPlugin): ISettingsPlugin;
|
|
5
|
+
constructor();
|
|
6
|
+
get ready(): Promise<void>;
|
|
7
|
+
initialize(): Promise<void>;
|
|
8
|
+
get(pluginId: string): Promise<ISettingsPlugin | undefined>;
|
|
9
|
+
getAll(): Promise<{
|
|
10
|
+
settings: ISettingsPlugin[];
|
|
11
|
+
}>;
|
|
12
|
+
private _getAll;
|
|
13
|
+
save(pluginId: string, raw: string): Promise<void>;
|
|
14
|
+
private _ready;
|
|
15
|
+
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
3
|
+
import * as json5 from 'json5';
|
|
4
|
+
// portions used from Jupyterlab:
|
|
5
|
+
/* -----------------------------------------------------------------------------
|
|
6
|
+
| Copyright (c) Jupyter Development Team.
|
|
7
|
+
| Distributed under the terms of the Modified BSD License.
|
|
8
|
+
|----------------------------------------------------------------------------*/
|
|
9
|
+
// This code contains portions from or is inspired by Jupyter lab and lite
|
|
10
|
+
class FailsSettings {
|
|
11
|
+
static override(plugin) {
|
|
12
|
+
if (FailsSettings._overrides[plugin.id]) {
|
|
13
|
+
if (!plugin.schema.properties) {
|
|
14
|
+
// probably malformed, or only provides keyboard shortcuts, etc.
|
|
15
|
+
plugin.schema.properties = {};
|
|
16
|
+
}
|
|
17
|
+
for (const [prop, propDefault] of Object.entries(FailsSettings._overrides[plugin.id] || {})) {
|
|
18
|
+
plugin.schema.properties[prop].default = propDefault;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return plugin;
|
|
22
|
+
}
|
|
23
|
+
constructor() {
|
|
24
|
+
this._ready = new PromiseDelegate();
|
|
25
|
+
}
|
|
26
|
+
get ready() {
|
|
27
|
+
return this._ready.promise;
|
|
28
|
+
}
|
|
29
|
+
async initialize() {
|
|
30
|
+
this._ready.resolve(void 0);
|
|
31
|
+
}
|
|
32
|
+
// copied from the original settings
|
|
33
|
+
async get(pluginId) {
|
|
34
|
+
const all = await this.getAll();
|
|
35
|
+
const settings = all.settings;
|
|
36
|
+
const setting = settings.find((setting) => {
|
|
37
|
+
return setting.id === pluginId;
|
|
38
|
+
});
|
|
39
|
+
return setting;
|
|
40
|
+
}
|
|
41
|
+
// copied from the original settings
|
|
42
|
+
async getAll() {
|
|
43
|
+
const allCore = await this._getAll('all.json');
|
|
44
|
+
let allFederated = [];
|
|
45
|
+
try {
|
|
46
|
+
allFederated = await this._getAll('all_federated.json');
|
|
47
|
+
}
|
|
48
|
+
catch (_a) {
|
|
49
|
+
// handle the case where there is no federated extension
|
|
50
|
+
}
|
|
51
|
+
// JupyterLab 4 expects all settings to be returned in one go
|
|
52
|
+
// so append the settings from federated plugins to the core ones
|
|
53
|
+
const all = allCore.concat(allFederated);
|
|
54
|
+
// return existing user settings if they exist
|
|
55
|
+
const settings = await Promise.all(all.map(async (plugin) => {
|
|
56
|
+
// const { id } = plugin;
|
|
57
|
+
const raw = /*((await storage.getItem(id)) as string) ?? */ plugin.raw;
|
|
58
|
+
return {
|
|
59
|
+
...FailsSettings.override(plugin),
|
|
60
|
+
raw,
|
|
61
|
+
settings: json5.parse(raw)
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
return { settings };
|
|
65
|
+
}
|
|
66
|
+
// one to one copy from settings of the original JupyterLite
|
|
67
|
+
async _getAll(file) {
|
|
68
|
+
var _a;
|
|
69
|
+
const settingsUrl = (_a = PageConfig.getOption('settingsUrl')) !== null && _a !== void 0 ? _a : '/';
|
|
70
|
+
const all = (await (await fetch(URLExt.join(settingsUrl, file))).json());
|
|
71
|
+
return all;
|
|
72
|
+
}
|
|
73
|
+
async save(pluginId, raw) {
|
|
74
|
+
// we do nothing
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// the following is copied from the original Jupyter Lite Settings Object
|
|
78
|
+
FailsSettings._overrides = JSON.parse(PageConfig.getOption('settingsOverrides') || '{}');
|
|
79
|
+
export { FailsSettings };
|
package/package.json
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fails-components/jupyter-filesystem-extension",
|
|
3
|
+
"version": "0.0.1-alpha.10",
|
|
4
|
+
"description": "A collection of extensions, that redirect's filesystems access to fails and let fails puppeteer Jupyter lite. ",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlite",
|
|
8
|
+
"jupyterlite-extension"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/fails-components/jupyterfails",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/fails-components/jupyterfails/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "BSD-3-Clause",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Marten Richter",
|
|
17
|
+
"email": "marten.richter@freenet.de"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
21
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
22
|
+
"schema/*.json",
|
|
23
|
+
"src/**/*.{ts,tsx}",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"main": "lib/index.js",
|
|
28
|
+
"types": "lib/index.d.ts",
|
|
29
|
+
"style": "style/index.css",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/fails-components/jupyterfails.git"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
36
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
37
|
+
"build:labextension": "jupyter labextension build .",
|
|
38
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
39
|
+
"build:lib": "tsc --sourceMap",
|
|
40
|
+
"build:lib:prod": "tsc",
|
|
41
|
+
"clean": "jlpm clean:lib",
|
|
42
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
43
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
44
|
+
"clean:labextension": "rimraf fails_components_jupyter_filesystem_extension/labextension fails_components_jupyter_filesystem_extension/_version.py",
|
|
45
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
46
|
+
"eslint": "jlpm eslint:check --fix",
|
|
47
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
48
|
+
"install:extension": "jlpm build",
|
|
49
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
50
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
51
|
+
"prepublishOnly": "npm run build",
|
|
52
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
53
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
54
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
55
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
56
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
57
|
+
"test": "jest --coverage --passWithNoTests",
|
|
58
|
+
"watch": "run-p watch:src watch:labextension",
|
|
59
|
+
"watch:src": "tsc -w --sourceMap",
|
|
60
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@fails-components/jupyter-launcher": "^0.0.1-alpha.10",
|
|
64
|
+
"@jupyter-widgets/jupyterlab-manager": "^5.0.11",
|
|
65
|
+
"@jupyterlab/application": "^4.3.4",
|
|
66
|
+
"@jupyterlab/services": "^7.3.4",
|
|
67
|
+
"@jupyterlite/application-extension": "^0.5.0",
|
|
68
|
+
"@jupyterlite/contents": "^0.5.0",
|
|
69
|
+
"@jupyterlite/server": "^0.5.0",
|
|
70
|
+
"@jupyterlite/settings": "^0.5.0",
|
|
71
|
+
"@lumino/coreutils": "^2.2.0",
|
|
72
|
+
"json5": "^2.2.3"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
76
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
77
|
+
"@types/jest": "^29.2.0",
|
|
78
|
+
"@types/json-schema": "^7.0.11",
|
|
79
|
+
"@types/react": "^18.0.26",
|
|
80
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
81
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
82
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
83
|
+
"css-loader": "^6.7.1",
|
|
84
|
+
"eslint": "^8.36.0",
|
|
85
|
+
"eslint-config-prettier": "^8.8.0",
|
|
86
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
87
|
+
"jest": "^29.2.0",
|
|
88
|
+
"npm-run-all": "^4.1.5",
|
|
89
|
+
"prettier": "^3.0.0",
|
|
90
|
+
"rimraf": "^5.0.1",
|
|
91
|
+
"source-map-loader": "^1.0.2",
|
|
92
|
+
"style-loader": "^3.3.1",
|
|
93
|
+
"stylelint": "^15.10.1",
|
|
94
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
95
|
+
"stylelint-config-standard": "^34.0.0",
|
|
96
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
97
|
+
"stylelint-prettier": "^4.0.0",
|
|
98
|
+
"typescript": "~5.0.2",
|
|
99
|
+
"yjs": "^13.5.0"
|
|
100
|
+
},
|
|
101
|
+
"sideEffects": [
|
|
102
|
+
"style/*.css",
|
|
103
|
+
"style/index.js"
|
|
104
|
+
],
|
|
105
|
+
"styleModule": "style/index.js",
|
|
106
|
+
"publishConfig": {
|
|
107
|
+
"access": "public"
|
|
108
|
+
},
|
|
109
|
+
"jupyterlab": {
|
|
110
|
+
"extension": true,
|
|
111
|
+
"outputDir": "fails_components_jupyter_filesystem_extension/labextension",
|
|
112
|
+
"schemaDir": "schema"
|
|
113
|
+
},
|
|
114
|
+
"jupyterlite": {
|
|
115
|
+
"liteExtension": true
|
|
116
|
+
},
|
|
117
|
+
"eslintIgnore": [
|
|
118
|
+
"node_modules",
|
|
119
|
+
"dist",
|
|
120
|
+
"coverage",
|
|
121
|
+
"**/*.d.ts",
|
|
122
|
+
"tests",
|
|
123
|
+
"**/__tests__",
|
|
124
|
+
"ui-tests"
|
|
125
|
+
],
|
|
126
|
+
"eslintConfig": {
|
|
127
|
+
"extends": [
|
|
128
|
+
"eslint:recommended",
|
|
129
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
130
|
+
"plugin:@typescript-eslint/recommended",
|
|
131
|
+
"plugin:prettier/recommended"
|
|
132
|
+
],
|
|
133
|
+
"parser": "@typescript-eslint/parser",
|
|
134
|
+
"parserOptions": {
|
|
135
|
+
"project": "tsconfig.json",
|
|
136
|
+
"sourceType": "module"
|
|
137
|
+
},
|
|
138
|
+
"plugins": [
|
|
139
|
+
"@typescript-eslint"
|
|
140
|
+
],
|
|
141
|
+
"rules": {
|
|
142
|
+
"@typescript-eslint/naming-convention": [
|
|
143
|
+
"error",
|
|
144
|
+
{
|
|
145
|
+
"selector": "interface",
|
|
146
|
+
"format": [
|
|
147
|
+
"PascalCase"
|
|
148
|
+
],
|
|
149
|
+
"custom": {
|
|
150
|
+
"regex": "^I[A-Z]",
|
|
151
|
+
"match": true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
"@typescript-eslint/no-unused-vars": [
|
|
156
|
+
"warn",
|
|
157
|
+
{
|
|
158
|
+
"args": "none"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
162
|
+
"@typescript-eslint/no-namespace": "off",
|
|
163
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
164
|
+
"@typescript-eslint/quotes": [
|
|
165
|
+
"error",
|
|
166
|
+
"single",
|
|
167
|
+
{
|
|
168
|
+
"avoidEscape": true,
|
|
169
|
+
"allowTemplateLiterals": false
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"curly": [
|
|
173
|
+
"error",
|
|
174
|
+
"all"
|
|
175
|
+
],
|
|
176
|
+
"eqeqeq": "error",
|
|
177
|
+
"prefer-arrow-callback": "error"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"prettier": {
|
|
181
|
+
"singleQuote": true,
|
|
182
|
+
"trailingComma": "none",
|
|
183
|
+
"arrowParens": "avoid",
|
|
184
|
+
"endOfLine": "auto",
|
|
185
|
+
"overrides": [
|
|
186
|
+
{
|
|
187
|
+
"files": "package.json",
|
|
188
|
+
"options": {
|
|
189
|
+
"tabWidth": 4
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"stylelint": {
|
|
195
|
+
"extends": [
|
|
196
|
+
"stylelint-config-recommended",
|
|
197
|
+
"stylelint-config-standard",
|
|
198
|
+
"stylelint-prettier/recommended"
|
|
199
|
+
],
|
|
200
|
+
"plugins": [
|
|
201
|
+
"stylelint-csstree-validator"
|
|
202
|
+
],
|
|
203
|
+
"rules": {
|
|
204
|
+
"csstree/validator": true,
|
|
205
|
+
"property-no-vendor-prefix": null,
|
|
206
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
207
|
+
"selector-no-vendor-prefix": null,
|
|
208
|
+
"value-no-vendor-prefix": null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/contents.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Contents as ServerContents } from '@jupyterlab/services';
|
|
2
|
+
import { IContents } from '@jupyterlite/contents';
|
|
3
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
4
|
+
import {
|
|
5
|
+
IFailsCallbacks,
|
|
6
|
+
IContentEventType,
|
|
7
|
+
ILoadJupyterContentEvent,
|
|
8
|
+
ISavedJupyterContentEvent
|
|
9
|
+
} from '@fails-components/jupyter-launcher';
|
|
10
|
+
|
|
11
|
+
// portions used from Jupyterlab:
|
|
12
|
+
/* -----------------------------------------------------------------------------
|
|
13
|
+
| Copyright (c) Jupyter Development Team.
|
|
14
|
+
| Distributed under the terms of the Modified BSD License.
|
|
15
|
+
|----------------------------------------------------------------------------*/
|
|
16
|
+
// This code contains portions from or is inspired by Jupyter lab and lite
|
|
17
|
+
|
|
18
|
+
const jsonMime = 'application/json';
|
|
19
|
+
|
|
20
|
+
export class FailsContents implements IContents {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._ready = new PromiseDelegate();
|
|
23
|
+
if (!(window as any).failsCallbacks) {
|
|
24
|
+
(window as any).failsCallbacks = {};
|
|
25
|
+
}
|
|
26
|
+
this._failsCallbacks = (window as any).failsCallbacks;
|
|
27
|
+
this._failsCallbacks.callContents = this.onMessage.bind(this);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async onMessage(event: IContentEventType): Promise<any> {
|
|
31
|
+
// todo handle events
|
|
32
|
+
switch (event.task) {
|
|
33
|
+
case 'loadFile':
|
|
34
|
+
{
|
|
35
|
+
const loadevent = event as ILoadJupyterContentEvent;
|
|
36
|
+
this._fileContent = JSON.stringify(
|
|
37
|
+
loadevent.fileData || FailsContents.EMPTY_NB
|
|
38
|
+
);
|
|
39
|
+
this._fileName = loadevent.fileName;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case 'savedFile':
|
|
43
|
+
{
|
|
44
|
+
const savedevent = event as ISavedJupyterContentEvent;
|
|
45
|
+
if (this._fileName !== savedevent.fileName) {
|
|
46
|
+
return { error: 'Filename not found' };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
fileData: JSON.parse(this._fileContent)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get ready(): Promise<void> {
|
|
57
|
+
return this._ready.promise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async initialize() {
|
|
61
|
+
this._ready.resolve(void 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async get(
|
|
65
|
+
path: string,
|
|
66
|
+
options?: ServerContents.IFetchOptions
|
|
67
|
+
): Promise<ServerContents.IModel | null> {
|
|
68
|
+
// remove leading slash
|
|
69
|
+
path = decodeURIComponent(path.replace(/^\//, ''));
|
|
70
|
+
|
|
71
|
+
const serverFile = {
|
|
72
|
+
name: this._fileName,
|
|
73
|
+
path: this._fileName,
|
|
74
|
+
last_modified: new Date(0).toISOString(),
|
|
75
|
+
created: new Date(0).toISOString(),
|
|
76
|
+
format: 'json' as ServerContents.FileFormat,
|
|
77
|
+
mimetype: jsonMime,
|
|
78
|
+
content: JSON.parse(this._fileContent),
|
|
79
|
+
size: 0,
|
|
80
|
+
writable: true,
|
|
81
|
+
type: 'notebook'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (path === '') {
|
|
85
|
+
// the local directory, return the info about the proxy notebook
|
|
86
|
+
return {
|
|
87
|
+
name: '',
|
|
88
|
+
path,
|
|
89
|
+
last_modified: new Date(0).toISOString(),
|
|
90
|
+
created: new Date(0).toISOString(),
|
|
91
|
+
format: 'json',
|
|
92
|
+
mimetype: jsonMime,
|
|
93
|
+
content: [serverFile],
|
|
94
|
+
size: 0,
|
|
95
|
+
writable: true,
|
|
96
|
+
type: 'directory'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (path === this._fileName) {
|
|
100
|
+
return serverFile;
|
|
101
|
+
}
|
|
102
|
+
return null; // not found
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async save(
|
|
106
|
+
path: string,
|
|
107
|
+
options: Partial<ServerContents.IModel> = {}
|
|
108
|
+
): Promise<ServerContents.IModel | null> {
|
|
109
|
+
path = decodeURIComponent(path);
|
|
110
|
+
if (path !== this._fileName) {
|
|
111
|
+
// we only allow the proxy object
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const chunk = options.chunk;
|
|
115
|
+
const chunked = chunk ? chunk > 1 || chunk === -1 : false;
|
|
116
|
+
|
|
117
|
+
let item: ServerContents.IModel | null = await this.get(path, {
|
|
118
|
+
content: chunked
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!item) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const modified = new Date().toISOString();
|
|
126
|
+
// override with the new values
|
|
127
|
+
item = {
|
|
128
|
+
...item,
|
|
129
|
+
...options,
|
|
130
|
+
last_modified: modified
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (options.content && options.format === 'base64') {
|
|
134
|
+
const lastChunk = chunk ? chunk === -1 : true;
|
|
135
|
+
|
|
136
|
+
const modified = new Date().toISOString();
|
|
137
|
+
// override with the new values
|
|
138
|
+
item = {
|
|
139
|
+
...item,
|
|
140
|
+
...options,
|
|
141
|
+
last_modified: modified
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const originalContent = item.content;
|
|
145
|
+
const escaped = decodeURIComponent(escape(atob(options.content)));
|
|
146
|
+
const newcontent = chunked ? originalContent + escaped : escaped;
|
|
147
|
+
item = {
|
|
148
|
+
...item,
|
|
149
|
+
content: lastChunk ? JSON.parse(newcontent) : newcontent,
|
|
150
|
+
format: 'json',
|
|
151
|
+
type: 'notebook',
|
|
152
|
+
size: newcontent.length
|
|
153
|
+
};
|
|
154
|
+
this._fileContent = JSON.stringify(newcontent); // no parsing
|
|
155
|
+
return item;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._fileContent = JSON.stringify(item.content); // no parsing
|
|
159
|
+
return item;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// For fails creating a new file is not allowed, so no need to implment it
|
|
163
|
+
async newUntitled(
|
|
164
|
+
options?: ServerContents.ICreateOptions
|
|
165
|
+
): Promise<ServerContents.IModel | null> {
|
|
166
|
+
throw new Error('NewUntitled not implemented');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async rename(
|
|
170
|
+
oldLocalPath: string,
|
|
171
|
+
newLocalPath: string
|
|
172
|
+
): Promise<ServerContents.IModel> {
|
|
173
|
+
throw new Error('rename not implemented');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async delete(path: string): Promise<void> {
|
|
177
|
+
throw new Error('delete not implemented');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async copy(path: string, toDir: string): Promise<ServerContents.IModel> {
|
|
181
|
+
throw new Error('copy not implemented');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async createCheckpoint(
|
|
185
|
+
path: string
|
|
186
|
+
): Promise<ServerContents.ICheckpointModel> {
|
|
187
|
+
throw new Error('createCheckpoint not (yet?) implemented');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async listCheckpoints(
|
|
191
|
+
path: string
|
|
192
|
+
): Promise<ServerContents.ICheckpointModel[]> {
|
|
193
|
+
// throw new Error('listCheckpoints not (yet?) implemented');
|
|
194
|
+
return [{ id: 'fakeCheckpoint', last_modified: new Date().toISOString() }];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async restoreCheckpoint(path: string, checkpointID: string): Promise<void> {
|
|
198
|
+
throw new Error('restoreCheckpoint not (yet?) implemented');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async deleteCheckpoint(path: string, checkpointID: string): Promise<void> {
|
|
202
|
+
throw new Error('deleteCheckpoint not (yet?) implemented');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static EMPTY_NB = {
|
|
206
|
+
metadata: {
|
|
207
|
+
orig_nbformat: 4
|
|
208
|
+
},
|
|
209
|
+
nbformat_minor: 5,
|
|
210
|
+
nbformat: 4,
|
|
211
|
+
cells: []
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
private _ready: PromiseDelegate<void>;
|
|
215
|
+
private _fileContent: string = JSON.stringify(FailsContents.EMPTY_NB);
|
|
216
|
+
private _fileName: string = 'unloaded.ipynb';
|
|
217
|
+
private _failsCallbacks: IFailsCallbacks;
|
|
218
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { IContents } from '@jupyterlite/contents';
|
|
2
|
+
import {
|
|
3
|
+
JupyterLiteServerPlugin,
|
|
4
|
+
JupyterLiteServer
|
|
5
|
+
} from '@jupyterlite/server';
|
|
6
|
+
import { ISettings } from '@jupyterlite/settings';
|
|
7
|
+
import { FailsContents } from './contents';
|
|
8
|
+
import { FailsSettings } from './settings';
|
|
9
|
+
|
|
10
|
+
export const failsContentsPlugin: JupyterLiteServerPlugin<IContents> = {
|
|
11
|
+
id: '@fails-components/jupyter-fails-server:contents',
|
|
12
|
+
requires: [],
|
|
13
|
+
autoStart: true,
|
|
14
|
+
provides: IContents,
|
|
15
|
+
activate: (app: JupyterLiteServer) => {
|
|
16
|
+
if (app.namespace !== 'JupyterLite Server') {
|
|
17
|
+
console.log('Not on server');
|
|
18
|
+
}
|
|
19
|
+
const contents = new FailsContents();
|
|
20
|
+
app.started.then(() => contents.initialize().catch(console.warn));
|
|
21
|
+
return contents;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const failsSettingsPlugin: JupyterLiteServerPlugin<ISettings> = {
|
|
26
|
+
id: '@fails-components/jupyter-fails-server:settings',
|
|
27
|
+
requires: [],
|
|
28
|
+
autoStart: true,
|
|
29
|
+
provides: ISettings,
|
|
30
|
+
activate: (app: JupyterLiteServer) => {
|
|
31
|
+
if (app.namespace !== 'JupyterLite Server') {
|
|
32
|
+
console.log('Not on server');
|
|
33
|
+
}
|
|
34
|
+
const settings = new FailsSettings();
|
|
35
|
+
app.started.then(() => settings.initialize().catch(console.warn));
|
|
36
|
+
return settings;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const plugins: JupyterLiteServerPlugin<any>[] = [
|
|
41
|
+
failsContentsPlugin,
|
|
42
|
+
failsSettingsPlugin
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export default plugins;
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ISettings, IPlugin as ISettingsPlugin } from '@jupyterlite/settings';
|
|
3
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
4
|
+
import * as json5 from 'json5';
|
|
5
|
+
|
|
6
|
+
// portions used from Jupyterlab:
|
|
7
|
+
/* -----------------------------------------------------------------------------
|
|
8
|
+
| Copyright (c) Jupyter Development Team.
|
|
9
|
+
| Distributed under the terms of the Modified BSD License.
|
|
10
|
+
|----------------------------------------------------------------------------*/
|
|
11
|
+
// This code contains portions from or is inspired by Jupyter lab and lite
|
|
12
|
+
|
|
13
|
+
export class FailsSettings implements ISettings {
|
|
14
|
+
// the following is copied from the original Jupyter Lite Settings Object
|
|
15
|
+
static _overrides: Record<string, ISettingsPlugin['schema']['default']> =
|
|
16
|
+
JSON.parse(PageConfig.getOption('settingsOverrides') || '{}');
|
|
17
|
+
|
|
18
|
+
static override(plugin: ISettingsPlugin): ISettingsPlugin {
|
|
19
|
+
if (FailsSettings._overrides[plugin.id]) {
|
|
20
|
+
if (!plugin.schema.properties) {
|
|
21
|
+
// probably malformed, or only provides keyboard shortcuts, etc.
|
|
22
|
+
plugin.schema.properties = {};
|
|
23
|
+
}
|
|
24
|
+
for (const [prop, propDefault] of Object.entries(
|
|
25
|
+
FailsSettings._overrides[plugin.id] || {}
|
|
26
|
+
)) {
|
|
27
|
+
plugin.schema.properties[prop].default = propDefault;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return plugin;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
this._ready = new PromiseDelegate();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get ready(): Promise<void> {
|
|
38
|
+
return this._ready.promise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async initialize() {
|
|
42
|
+
this._ready.resolve(void 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// copied from the original settings
|
|
46
|
+
async get(pluginId: string): Promise<ISettingsPlugin | undefined> {
|
|
47
|
+
const all = await this.getAll();
|
|
48
|
+
const settings = all.settings as ISettingsPlugin[];
|
|
49
|
+
const setting = settings.find((setting: ISettingsPlugin) => {
|
|
50
|
+
return setting.id === pluginId;
|
|
51
|
+
});
|
|
52
|
+
return setting;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// copied from the original settings
|
|
56
|
+
async getAll(): Promise<{ settings: ISettingsPlugin[] }> {
|
|
57
|
+
const allCore = await this._getAll('all.json');
|
|
58
|
+
let allFederated: ISettingsPlugin[] = [];
|
|
59
|
+
try {
|
|
60
|
+
allFederated = await this._getAll('all_federated.json');
|
|
61
|
+
} catch {
|
|
62
|
+
// handle the case where there is no federated extension
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// JupyterLab 4 expects all settings to be returned in one go
|
|
66
|
+
// so append the settings from federated plugins to the core ones
|
|
67
|
+
const all = allCore.concat(allFederated);
|
|
68
|
+
|
|
69
|
+
// return existing user settings if they exist
|
|
70
|
+
const settings = await Promise.all(
|
|
71
|
+
all.map(async plugin => {
|
|
72
|
+
// const { id } = plugin;
|
|
73
|
+
const raw = /*((await storage.getItem(id)) as string) ?? */ plugin.raw;
|
|
74
|
+
return {
|
|
75
|
+
...FailsSettings.override(plugin),
|
|
76
|
+
raw,
|
|
77
|
+
settings: json5.parse(raw)
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
return { settings };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// one to one copy from settings of the original JupyterLite
|
|
85
|
+
private async _getAll(
|
|
86
|
+
file: 'all.json' | 'all_federated.json'
|
|
87
|
+
): Promise<ISettingsPlugin[]> {
|
|
88
|
+
const settingsUrl = PageConfig.getOption('settingsUrl') ?? '/';
|
|
89
|
+
const all = (await (
|
|
90
|
+
await fetch(URLExt.join(settingsUrl, file))
|
|
91
|
+
).json()) as ISettingsPlugin[];
|
|
92
|
+
return all;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async save(pluginId: string, raw: string): Promise<void> {
|
|
96
|
+
// we do nothing
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _ready: PromiseDelegate<void>;
|
|
100
|
+
}
|
package/style/base.css
ADDED
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED