@jupyterlab/settingeditor 4.0.0-alpha.2 → 4.0.0-alpha.21
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/SettingsFormEditor.d.ts +101 -0
- package/lib/SettingsFormEditor.js +151 -0
- package/lib/SettingsFormEditor.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/inspector.js +24 -30
- package/lib/inspector.js.map +1 -1
- package/lib/{settingeditor.d.ts → jsonsettingeditor.d.ts} +5 -6
- package/lib/{settingeditor.js → jsonsettingeditor.js} +25 -43
- package/lib/jsonsettingeditor.js.map +1 -0
- package/lib/plugineditor.d.ts +3 -3
- package/lib/plugineditor.js +2 -6
- package/lib/plugineditor.js.map +1 -1
- package/lib/pluginlist.d.ts +58 -22
- package/lib/pluginlist.js +246 -109
- package/lib/pluginlist.js.map +1 -1
- package/lib/raweditor.d.ts +2 -6
- package/lib/raweditor.js +22 -33
- package/lib/raweditor.js.map +1 -1
- package/lib/settingseditor.d.ts +81 -0
- package/lib/settingseditor.js +121 -0
- package/lib/settingseditor.js.map +1 -0
- package/lib/settingspanel.d.ts +54 -0
- package/lib/settingspanel.js +102 -0
- package/lib/settingspanel.js.map +1 -0
- package/lib/tokens.d.ts +12 -2
- package/lib/tokens.js +4 -1
- package/lib/tokens.js.map +1 -1
- package/package.json +28 -21
- package/src/SettingsFormEditor.tsx +306 -0
- package/src/index.ts +10 -0
- package/src/inspector.ts +144 -0
- package/src/jsonsettingeditor.tsx +482 -0
- package/src/plugineditor.ts +257 -0
- package/src/pluginlist.tsx +538 -0
- package/src/raweditor.ts +422 -0
- package/src/settingseditor.tsx +210 -0
- package/src/settingspanel.tsx +218 -0
- package/src/tokens.ts +33 -0
- package/style/base.css +213 -89
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/lib/settingeditor.js.map +0 -1
- package/lib/splitpanel.d.ts +0 -13
- package/lib/splitpanel.js +0 -26
- package/lib/splitpanel.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyterlab/settingeditor",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.21",
|
|
4
4
|
"description": "The JupyterLab default setting editor interface",
|
|
5
5
|
"homepage": "https://github.com/jupyterlab/jupyterlab",
|
|
6
6
|
"bugs": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"lib/*.js.map",
|
|
28
28
|
"lib/*.js",
|
|
29
29
|
"style/*.css",
|
|
30
|
-
"style/index.js"
|
|
30
|
+
"style/index.js",
|
|
31
|
+
"src/**/*.{ts,tsx}"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "tsc -b",
|
|
@@ -36,28 +37,34 @@
|
|
|
36
37
|
"watch": "tsc -b --watch"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@jupyterlab/
|
|
40
|
-
"@jupyterlab/
|
|
41
|
-
"@jupyterlab/
|
|
42
|
-
"@jupyterlab/
|
|
43
|
-
"@jupyterlab/
|
|
44
|
-
"@jupyterlab/
|
|
45
|
-
"@jupyterlab/
|
|
46
|
-
"@jupyterlab/
|
|
47
|
-
"@
|
|
48
|
-
"@lumino/
|
|
49
|
-
"@lumino/
|
|
50
|
-
"@lumino/
|
|
51
|
-
"@lumino/
|
|
52
|
-
"
|
|
53
|
-
"
|
|
40
|
+
"@jupyterlab/application": "^4.0.0-alpha.21",
|
|
41
|
+
"@jupyterlab/apputils": "^4.0.0-alpha.21",
|
|
42
|
+
"@jupyterlab/codeeditor": "^4.0.0-alpha.21",
|
|
43
|
+
"@jupyterlab/inspector": "^4.0.0-alpha.21",
|
|
44
|
+
"@jupyterlab/rendermime": "^4.0.0-alpha.21",
|
|
45
|
+
"@jupyterlab/settingregistry": "^4.0.0-alpha.21",
|
|
46
|
+
"@jupyterlab/statedb": "^4.0.0-alpha.21",
|
|
47
|
+
"@jupyterlab/translation": "^4.0.0-alpha.21",
|
|
48
|
+
"@jupyterlab/ui-components": "^4.0.0-alpha.36",
|
|
49
|
+
"@lumino/algorithm": "^2.0.0-rc.1",
|
|
50
|
+
"@lumino/commands": "^2.0.0-rc.1",
|
|
51
|
+
"@lumino/coreutils": "^2.0.0-rc.1",
|
|
52
|
+
"@lumino/disposable": "^2.0.0-rc.1",
|
|
53
|
+
"@lumino/messaging": "^2.0.0-rc.1",
|
|
54
|
+
"@lumino/polling": "^2.0.0-rc.1",
|
|
55
|
+
"@lumino/signaling": "^2.0.0-rc.1",
|
|
56
|
+
"@lumino/widgets": "^2.0.0-rc.1",
|
|
57
|
+
"@rjsf/core": "^5.1.0",
|
|
58
|
+
"@rjsf/utils": "^5.1.0",
|
|
59
|
+
"@rjsf/validator-ajv8": "^5.1.0",
|
|
60
|
+
"json-schema": "^0.4.0",
|
|
61
|
+
"react": "^18.2.0"
|
|
54
62
|
},
|
|
55
63
|
"devDependencies": {
|
|
56
|
-
"@types/react": "^
|
|
57
|
-
"@types/react-dom": "^17.0.0",
|
|
64
|
+
"@types/react": "^18.0.26",
|
|
58
65
|
"rimraf": "~3.0.0",
|
|
59
|
-
"typedoc": "~0.
|
|
60
|
-
"typescript": "~
|
|
66
|
+
"typedoc": "~0.23.25",
|
|
67
|
+
"typescript": "~5.0.1-rc"
|
|
61
68
|
},
|
|
62
69
|
"publishConfig": {
|
|
63
70
|
"access": "public"
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import { showErrorMessage } from '@jupyterlab/apputils';
|
|
7
|
+
import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
|
|
8
|
+
import { ITranslator } from '@jupyterlab/translation';
|
|
9
|
+
import {
|
|
10
|
+
caretDownIcon,
|
|
11
|
+
caretRightIcon,
|
|
12
|
+
FormComponent
|
|
13
|
+
} from '@jupyterlab/ui-components';
|
|
14
|
+
import { JSONExt, ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
15
|
+
import { Debouncer } from '@lumino/polling';
|
|
16
|
+
import { IChangeEvent } from '@rjsf/core';
|
|
17
|
+
import validatorAjv8 from '@rjsf/validator-ajv8';
|
|
18
|
+
import { Field, UiSchema } from '@rjsf/utils';
|
|
19
|
+
import { JSONSchema7 } from 'json-schema';
|
|
20
|
+
import React from 'react';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Indentation to use when saving the settings as JSON document.
|
|
24
|
+
*/
|
|
25
|
+
const JSON_INDENTATION = 4;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Namespace for a React component that prepares the settings for a
|
|
29
|
+
* given plugin to be rendered in the FormEditor.
|
|
30
|
+
*/
|
|
31
|
+
export namespace SettingsFormEditor {
|
|
32
|
+
/**
|
|
33
|
+
* Props passed to the SettingsFormEditor component
|
|
34
|
+
*/
|
|
35
|
+
export interface IProps {
|
|
36
|
+
/**
|
|
37
|
+
* Settings object with schema and user defined values.
|
|
38
|
+
*/
|
|
39
|
+
settings: Settings;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Dictionary used for custom field renderers in the form.
|
|
43
|
+
*/
|
|
44
|
+
renderers: { [id: string]: { [property: string]: Field } };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether the form is collapsed or not.
|
|
48
|
+
*/
|
|
49
|
+
isCollapsed: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Callback with the collapse state value.
|
|
53
|
+
*/
|
|
54
|
+
onCollapseChange: (v: boolean) => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Translator object
|
|
58
|
+
*/
|
|
59
|
+
translator: ITranslator;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Callback to update the plugin list when a validation error occurs.
|
|
63
|
+
*/
|
|
64
|
+
hasError: (error: boolean) => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handler for when selection change is triggered by scrolling
|
|
68
|
+
* in the SettingsPanel.
|
|
69
|
+
*/
|
|
70
|
+
onSelect: (id: string) => void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sends whether this editor has unsaved changes to the parent class.
|
|
74
|
+
*/
|
|
75
|
+
updateDirtyState: (dirty: boolean) => void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* List of strings that match search value.
|
|
79
|
+
*/
|
|
80
|
+
filteredValues: string[] | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface IState {
|
|
84
|
+
/**
|
|
85
|
+
* Indicates whether the settings have been modified. Used for hiding
|
|
86
|
+
* the "Restore to Default" button when there are no changes.
|
|
87
|
+
*/
|
|
88
|
+
isModified: boolean;
|
|
89
|
+
|
|
90
|
+
// The following are state derived from props for memoization to avoid
|
|
91
|
+
// `Form` update that results in focus lost
|
|
92
|
+
// A better fix (that will break the API) would be to move this as props
|
|
93
|
+
// of the component
|
|
94
|
+
/**
|
|
95
|
+
* Form UI schema
|
|
96
|
+
*/
|
|
97
|
+
uiSchema: UiSchema;
|
|
98
|
+
/**
|
|
99
|
+
* Filtered schema
|
|
100
|
+
*/
|
|
101
|
+
filteredSchema?: ISettingRegistry.ISchema;
|
|
102
|
+
/**
|
|
103
|
+
* Form context
|
|
104
|
+
*/
|
|
105
|
+
formContext?: any;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A React component that prepares the settings for a
|
|
111
|
+
* given plugin to be rendered in the FormEditor.
|
|
112
|
+
*/
|
|
113
|
+
export class SettingsFormEditor extends React.Component<
|
|
114
|
+
SettingsFormEditor.IProps,
|
|
115
|
+
SettingsFormEditor.IState
|
|
116
|
+
> {
|
|
117
|
+
constructor(props: SettingsFormEditor.IProps) {
|
|
118
|
+
super(props);
|
|
119
|
+
const { settings } = props;
|
|
120
|
+
this._formData = settings.composite;
|
|
121
|
+
this.state = {
|
|
122
|
+
isModified: settings.isModified,
|
|
123
|
+
uiSchema: {},
|
|
124
|
+
filteredSchema: this.props.settings.schema,
|
|
125
|
+
formContext: {
|
|
126
|
+
defaultFormData: this.props.settings.default(),
|
|
127
|
+
settings: this.props.settings
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
this.handleChange = this.handleChange.bind(this);
|
|
131
|
+
this._debouncer = new Debouncer(this.handleChange);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
componentDidMount(): void {
|
|
135
|
+
this._setUiSchema();
|
|
136
|
+
this._setFilteredSchema();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
componentDidUpdate(prevProps: SettingsFormEditor.IProps): void {
|
|
140
|
+
this._setUiSchema(prevProps.renderers[prevProps.settings.id]);
|
|
141
|
+
this._setFilteredSchema(prevProps.filteredValues);
|
|
142
|
+
|
|
143
|
+
if (prevProps.settings !== this.props.settings) {
|
|
144
|
+
this.setState({
|
|
145
|
+
formContext: {
|
|
146
|
+
settings: this.props.settings,
|
|
147
|
+
defaultFormData: this.props.settings.default()
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
componentWillUnmount(): void {
|
|
154
|
+
this._debouncer.dispose();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Handler for edits made in the form editor.
|
|
159
|
+
*/
|
|
160
|
+
handleChange(): void {
|
|
161
|
+
// Prevent unnecessary save when opening settings that haven't been modified.
|
|
162
|
+
if (
|
|
163
|
+
!this.props.settings.isModified &&
|
|
164
|
+
this.props.settings.isDefault(this._formData)
|
|
165
|
+
) {
|
|
166
|
+
this.props.updateDirtyState(false);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.props.settings
|
|
170
|
+
.save(JSON.stringify(this._formData, undefined, JSON_INDENTATION))
|
|
171
|
+
.then(() => {
|
|
172
|
+
this.props.updateDirtyState(false);
|
|
173
|
+
this.setState({ isModified: this.props.settings.isModified });
|
|
174
|
+
})
|
|
175
|
+
.catch((reason: string) => {
|
|
176
|
+
this.props.updateDirtyState(false);
|
|
177
|
+
const trans = this.props.translator.load('jupyterlab');
|
|
178
|
+
void showErrorMessage(trans.__('Error saving settings.'), reason);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handler for the "Restore to defaults" button - clears all
|
|
184
|
+
* modified settings then calls `setFormData` to restore the
|
|
185
|
+
* values.
|
|
186
|
+
*/
|
|
187
|
+
reset = async (event: React.MouseEvent): Promise<void> => {
|
|
188
|
+
event.stopPropagation();
|
|
189
|
+
for (const field in this.props.settings.user) {
|
|
190
|
+
await this.props.settings.remove(field);
|
|
191
|
+
}
|
|
192
|
+
this._formData = this.props.settings.composite;
|
|
193
|
+
this.setState({ isModified: false });
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
render(): JSX.Element {
|
|
197
|
+
const trans = this.props.translator.load('jupyterlab');
|
|
198
|
+
const icon = this.props.isCollapsed ? caretRightIcon : caretDownIcon;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<div
|
|
203
|
+
className="jp-SettingsHeader"
|
|
204
|
+
onClick={() => {
|
|
205
|
+
this.props.onCollapseChange(!this.props.isCollapsed);
|
|
206
|
+
this.props.onSelect(this.props.settings.id);
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<header className="jp-SettingsTitle">
|
|
210
|
+
<icon.react
|
|
211
|
+
tag="span"
|
|
212
|
+
elementPosition="center"
|
|
213
|
+
className="jp-SettingsTitle-caret"
|
|
214
|
+
/>
|
|
215
|
+
<h2>{this.props.settings.schema.title}</h2>
|
|
216
|
+
<div className="jp-SettingsHeader-description">
|
|
217
|
+
{this.props.settings.schema.description}
|
|
218
|
+
</div>
|
|
219
|
+
</header>
|
|
220
|
+
{this.state.isModified && (
|
|
221
|
+
<button className="jp-RestoreButton" onClick={this.reset}>
|
|
222
|
+
{trans.__('Restore to Defaults')}
|
|
223
|
+
</button>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
{!this.props.isCollapsed && (
|
|
227
|
+
<FormComponent
|
|
228
|
+
validator={validatorAjv8}
|
|
229
|
+
schema={this.state.filteredSchema as JSONSchema7}
|
|
230
|
+
formData={this._formData}
|
|
231
|
+
uiSchema={this.state.uiSchema}
|
|
232
|
+
fields={this.props.renderers[this.props.settings.id]}
|
|
233
|
+
formContext={this.state.formContext}
|
|
234
|
+
liveValidate
|
|
235
|
+
idPrefix={`jp-SettingsEditor-${this.props.settings.id}`}
|
|
236
|
+
onChange={this._onChange}
|
|
237
|
+
translator={this.props.translator}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private _onChange = (e: IChangeEvent<ReadonlyPartialJSONObject>): void => {
|
|
245
|
+
this.props.hasError(e.errors.length !== 0);
|
|
246
|
+
this._formData = e.formData;
|
|
247
|
+
if (e.errors.length === 0) {
|
|
248
|
+
this.props.updateDirtyState(true);
|
|
249
|
+
void this._debouncer.invoke();
|
|
250
|
+
}
|
|
251
|
+
this.props.onSelect(this.props.settings.id);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
private _setUiSchema(prevRenderers?: { [id: string]: Field }) {
|
|
255
|
+
const renderers = this.props.renderers[this.props.settings.id];
|
|
256
|
+
if (
|
|
257
|
+
!JSONExt.deepEqual(
|
|
258
|
+
Object.keys(prevRenderers ?? {}).sort(),
|
|
259
|
+
Object.keys(renderers ?? {}).sort()
|
|
260
|
+
)
|
|
261
|
+
) {
|
|
262
|
+
/**
|
|
263
|
+
* Construct uiSchema to pass any custom renderers to the form editor.
|
|
264
|
+
*/
|
|
265
|
+
const uiSchema: UiSchema = {};
|
|
266
|
+
for (const id in this.props.renderers[this.props.settings.id]) {
|
|
267
|
+
if (
|
|
268
|
+
Object.keys(this.props.settings.schema.properties ?? {}).includes(id)
|
|
269
|
+
) {
|
|
270
|
+
uiSchema[id] = {
|
|
271
|
+
'ui:field': id
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.setState({ uiSchema });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _setFilteredSchema(prevFilteredValues?: string[] | null) {
|
|
280
|
+
if (
|
|
281
|
+
prevFilteredValues === undefined ||
|
|
282
|
+
!JSONExt.deepEqual(prevFilteredValues, this.props.filteredValues)
|
|
283
|
+
) {
|
|
284
|
+
/**
|
|
285
|
+
* Only show fields that match search value.
|
|
286
|
+
*/
|
|
287
|
+
const filteredSchema = JSONExt.deepCopy(this.props.settings.schema);
|
|
288
|
+
if (this.props.filteredValues?.length ?? 0 > 0) {
|
|
289
|
+
for (const field in filteredSchema.properties) {
|
|
290
|
+
if (
|
|
291
|
+
!this.props.filteredValues?.includes(
|
|
292
|
+
filteredSchema.properties[field].title ?? field
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
delete filteredSchema.properties[field];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.setState({ filteredSchema });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private _debouncer: Debouncer<void, any>;
|
|
305
|
+
private _formData: any;
|
|
306
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
/**
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @module settingeditor
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './settingseditor';
|
|
9
|
+
export * from './jsonsettingeditor';
|
|
10
|
+
export * from './tokens';
|
package/src/inspector.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* -----------------------------------------------------------------------------
|
|
2
|
+
| Copyright (c) Jupyter Development Team.
|
|
3
|
+
| Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|----------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import { InspectionHandler, InspectorPanel } from '@jupyterlab/inspector';
|
|
7
|
+
import {
|
|
8
|
+
IRenderMimeRegistry,
|
|
9
|
+
RenderMimeRegistry,
|
|
10
|
+
standardRendererFactories
|
|
11
|
+
} from '@jupyterlab/rendermime';
|
|
12
|
+
import { ISchemaValidator } from '@jupyterlab/settingregistry';
|
|
13
|
+
import { DataConnector } from '@jupyterlab/statedb';
|
|
14
|
+
import {
|
|
15
|
+
ITranslator,
|
|
16
|
+
nullTranslator,
|
|
17
|
+
TranslationBundle
|
|
18
|
+
} from '@jupyterlab/translation';
|
|
19
|
+
import { ReadonlyJSONObject } from '@lumino/coreutils';
|
|
20
|
+
import { RawEditor } from './raweditor';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a raw editor inspector.
|
|
24
|
+
*/
|
|
25
|
+
export function createInspector(
|
|
26
|
+
editor: RawEditor,
|
|
27
|
+
rendermime?: IRenderMimeRegistry,
|
|
28
|
+
translator?: ITranslator
|
|
29
|
+
): InspectorPanel {
|
|
30
|
+
translator = translator || nullTranslator;
|
|
31
|
+
const trans = translator.load('jupyterlab');
|
|
32
|
+
const connector = new InspectorConnector(editor, translator);
|
|
33
|
+
const inspector = new InspectorPanel({
|
|
34
|
+
initialContent: trans.__('Any errors will be listed here'),
|
|
35
|
+
translator: translator
|
|
36
|
+
});
|
|
37
|
+
const handler = new InspectionHandler({
|
|
38
|
+
connector,
|
|
39
|
+
rendermime:
|
|
40
|
+
rendermime ||
|
|
41
|
+
new RenderMimeRegistry({
|
|
42
|
+
initialFactories: standardRendererFactories,
|
|
43
|
+
translator: translator
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
inspector.addClass('jp-SettingsDebug');
|
|
48
|
+
inspector.source = handler;
|
|
49
|
+
handler.editor = editor.source;
|
|
50
|
+
|
|
51
|
+
return inspector;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The data connector used to populate a code inspector.
|
|
56
|
+
*
|
|
57
|
+
* #### Notes
|
|
58
|
+
* This data connector debounces fetch requests to throttle them at no more than
|
|
59
|
+
* one request per 100ms. This means that using the connector to populate
|
|
60
|
+
* multiple client objects can lead to missed fetch responses.
|
|
61
|
+
*/
|
|
62
|
+
class InspectorConnector extends DataConnector<
|
|
63
|
+
InspectionHandler.IReply,
|
|
64
|
+
void,
|
|
65
|
+
InspectionHandler.IRequest
|
|
66
|
+
> {
|
|
67
|
+
constructor(editor: RawEditor, translator?: ITranslator) {
|
|
68
|
+
super();
|
|
69
|
+
this._editor = editor;
|
|
70
|
+
this._trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch inspection requests.
|
|
75
|
+
*/
|
|
76
|
+
fetch(
|
|
77
|
+
request: InspectionHandler.IRequest
|
|
78
|
+
): Promise<InspectionHandler.IReply | undefined> {
|
|
79
|
+
return new Promise<InspectionHandler.IReply | undefined>(resolve => {
|
|
80
|
+
// Debounce requests at a rate of 100ms.
|
|
81
|
+
const current = (this._current = window.setTimeout(() => {
|
|
82
|
+
if (current !== this._current) {
|
|
83
|
+
return resolve(undefined);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const errors = this._validate(request.text);
|
|
87
|
+
|
|
88
|
+
if (!errors) {
|
|
89
|
+
return resolve({
|
|
90
|
+
data: { 'text/markdown': this._trans.__('No errors found') },
|
|
91
|
+
metadata: {}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolve({ data: this.render(errors), metadata: {} });
|
|
96
|
+
}, 100));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Render validation errors as an HTML string.
|
|
101
|
+
*/
|
|
102
|
+
protected render(errors: ISchemaValidator.IError[]): ReadonlyJSONObject {
|
|
103
|
+
return {
|
|
104
|
+
'text/markdown': errors.map(this.renderError.bind(this)).join('')
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Render an individual validation error as a markdown string.
|
|
110
|
+
*/
|
|
111
|
+
protected renderError(error: ISchemaValidator.IError): string {
|
|
112
|
+
switch (error.keyword) {
|
|
113
|
+
case 'additionalProperties':
|
|
114
|
+
return `**\`[${this._trans.__('additional property error')}]\`**
|
|
115
|
+
${this._trans.__(
|
|
116
|
+
'`%1` is not a valid property',
|
|
117
|
+
error.params?.additionalProperty
|
|
118
|
+
)}`;
|
|
119
|
+
case 'syntax':
|
|
120
|
+
return `**\`[${this._trans.__('syntax error')}]\`** *${error.message}*`;
|
|
121
|
+
case 'type':
|
|
122
|
+
return `**\`[${this._trans.__('type error')}]\`**
|
|
123
|
+
\`${error.instancePath}\` ${error.message}`;
|
|
124
|
+
default:
|
|
125
|
+
return `**\`[${this._trans.__('error')}]\`** *${error.message}*`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private _validate(raw: string): ISchemaValidator.IError[] | null {
|
|
130
|
+
const editor = this._editor;
|
|
131
|
+
if (!editor.settings) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const { id, schema, version } = editor.settings;
|
|
135
|
+
const data = { composite: {}, user: {} };
|
|
136
|
+
const validator = editor.registry.validator;
|
|
137
|
+
|
|
138
|
+
return validator.validateData({ data, id, raw, schema, version }, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _trans: TranslationBundle;
|
|
142
|
+
private _current = 0;
|
|
143
|
+
private _editor: RawEditor;
|
|
144
|
+
}
|