@jupyterlab/apputils-extension 4.0.0-alpha.2 → 4.0.0-alpha.20
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/announcements.d.ts +2 -0
- package/lib/announcements.js +227 -0
- package/lib/announcements.js.map +1 -0
- package/lib/index.js +39 -8
- package/lib/index.js.map +1 -1
- package/lib/notificationplugin.d.ts +5 -0
- package/lib/notificationplugin.js +504 -0
- package/lib/notificationplugin.js.map +1 -0
- package/lib/palette.js +3 -3
- package/lib/palette.js.map +1 -1
- package/lib/settingconnector.js +4 -0
- package/lib/settingconnector.js.map +1 -1
- package/lib/settingsplugin.js +8 -2
- package/lib/settingsplugin.js.map +1 -1
- package/lib/statusbarplugin.d.ts +7 -0
- package/lib/statusbarplugin.js +96 -0
- package/lib/statusbarplugin.js.map +1 -0
- package/lib/themesplugins.js +3 -0
- package/lib/themesplugins.js.map +1 -1
- package/lib/toolbarregistryplugin.js +5 -1
- package/lib/toolbarregistryplugin.js.map +1 -1
- package/lib/workspacesplugin.js +6 -6
- package/lib/workspacesplugin.js.map +1 -1
- package/package.json +26 -22
- package/schema/notification.json +48 -0
- package/schema/palette.json +2 -0
- package/schema/sanitizer.json +19 -0
- package/src/announcements.ts +297 -0
- package/src/index.ts +674 -0
- package/src/notificationplugin.tsx +902 -0
- package/src/palette.ts +213 -0
- package/src/settingconnector.ts +63 -0
- package/src/settingsplugin.ts +65 -0
- package/src/statusbarplugin.ts +145 -0
- package/src/themesplugins.ts +266 -0
- package/src/toolbarregistryplugin.ts +29 -0
- package/src/workspacesplugin.ts +306 -0
- package/style/base.css +1 -1
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/style/notification.css +227 -0
- package/style/splash.css +4 -0
- package/style/redirect.css +0 -15
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
JupyterFrontEnd,
|
|
8
|
+
JupyterFrontEndPlugin
|
|
9
|
+
} from '@jupyterlab/application';
|
|
10
|
+
import { Notification } from '@jupyterlab/apputils';
|
|
11
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
12
|
+
import { ConfigSection, ServerConnection } from '@jupyterlab/services';
|
|
13
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
14
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
15
|
+
|
|
16
|
+
const COMMAND_HELP_OPEN = 'help:open';
|
|
17
|
+
const NEWS_API_URL = '/lab/api/news';
|
|
18
|
+
const UPDATE_API_URL = '/lab/api/update';
|
|
19
|
+
const PRIVACY_URL =
|
|
20
|
+
'https://jupyterlab.readthedocs.io/en/latest/privacy_policies.html';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Call the announcement API
|
|
24
|
+
*
|
|
25
|
+
* @param endpoint Endpoint to request
|
|
26
|
+
* @param init Initial values for the request
|
|
27
|
+
* @returns The response body interpreted as JSON
|
|
28
|
+
*/
|
|
29
|
+
async function requestAPI<T>(
|
|
30
|
+
endpoint: string,
|
|
31
|
+
init: RequestInit = {}
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
// Make request to Jupyter API
|
|
34
|
+
const settings = ServerConnection.makeSettings();
|
|
35
|
+
const requestUrl = URLExt.join(settings.baseUrl, endpoint);
|
|
36
|
+
|
|
37
|
+
let response: Response;
|
|
38
|
+
try {
|
|
39
|
+
response = await ServerConnection.makeRequest(requestUrl, init, settings);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new ServerConnection.NetworkError(error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new ServerConnection.ResponseError(response, data.message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const announcements: JupyterFrontEndPlugin<void> = {
|
|
54
|
+
id: '@jupyterlab/apputils-extension:announcements',
|
|
55
|
+
autoStart: true,
|
|
56
|
+
optional: [ISettingRegistry, ITranslator],
|
|
57
|
+
activate: (
|
|
58
|
+
app: JupyterFrontEnd,
|
|
59
|
+
settingRegistry: ISettingRegistry | null,
|
|
60
|
+
translator: ITranslator | null
|
|
61
|
+
): void => {
|
|
62
|
+
const CONFIG_SECTION_NAME = announcements.id.replace(/[^\w]/g, '');
|
|
63
|
+
|
|
64
|
+
void Promise.all([
|
|
65
|
+
app.restored,
|
|
66
|
+
settingRegistry?.load('@jupyterlab/apputils-extension:notification') ??
|
|
67
|
+
Promise.resolve(null),
|
|
68
|
+
// Use config instead of state to store independently of the workspace
|
|
69
|
+
// if a news has been displayed or not.
|
|
70
|
+
ConfigSection.create({
|
|
71
|
+
name: CONFIG_SECTION_NAME
|
|
72
|
+
})
|
|
73
|
+
]).then(async ([_, settings, config]) => {
|
|
74
|
+
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
75
|
+
|
|
76
|
+
// Store dismiss state
|
|
77
|
+
Notification.manager.changed.connect((manager, change) => {
|
|
78
|
+
if (change.type !== 'removed') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { id, tags }: { id?: string; tags?: Array<string> } = (change
|
|
82
|
+
.notification.options.data ?? {}) as any;
|
|
83
|
+
if ((tags ?? []).some(tag => ['news', 'update'].includes(tag)) && id) {
|
|
84
|
+
const update: { [k: string]: INewsState } = {};
|
|
85
|
+
update[id] = { seen: true, dismissed: true };
|
|
86
|
+
config.update(update as any).catch(reason => {
|
|
87
|
+
console.error(
|
|
88
|
+
`Failed to update the announcements config:\n${reason}`
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const mustFetchNews = settings?.get('fetchNews').composite as
|
|
95
|
+
| 'true'
|
|
96
|
+
| 'false'
|
|
97
|
+
| 'none';
|
|
98
|
+
if (mustFetchNews === 'none') {
|
|
99
|
+
const notificationId = Notification.emit(
|
|
100
|
+
trans.__(
|
|
101
|
+
'Would you like to receive official Jupyter news?\nPlease read the privacy policy.'
|
|
102
|
+
),
|
|
103
|
+
'default',
|
|
104
|
+
{
|
|
105
|
+
autoClose: false,
|
|
106
|
+
actions: [
|
|
107
|
+
{
|
|
108
|
+
label: trans.__('Open privacy policy'),
|
|
109
|
+
caption: PRIVACY_URL,
|
|
110
|
+
callback: event => {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
if (app.commands.hasCommand(COMMAND_HELP_OPEN)) {
|
|
113
|
+
void app.commands.execute(COMMAND_HELP_OPEN, {
|
|
114
|
+
text: trans.__('Privacy policies'),
|
|
115
|
+
url: PRIVACY_URL
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
window.open(PRIVACY_URL, '_blank', 'noreferrer');
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
displayType: 'link'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: trans.__('Yes'),
|
|
125
|
+
callback: () => {
|
|
126
|
+
Notification.dismiss(notificationId);
|
|
127
|
+
config
|
|
128
|
+
.update({})
|
|
129
|
+
.then(() => fetchNews())
|
|
130
|
+
.catch(reason => {
|
|
131
|
+
console.error(`Failed to get the news:\n${reason}`);
|
|
132
|
+
});
|
|
133
|
+
settings?.set('fetchNews', 'true').catch((reason: any) => {
|
|
134
|
+
console.error(
|
|
135
|
+
`Failed to save setting 'fetchNews':\n${reason}`
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
label: trans.__('No'),
|
|
142
|
+
callback: () => {
|
|
143
|
+
Notification.dismiss(notificationId);
|
|
144
|
+
settings?.set('fetchNews', 'false').catch((reason: any) => {
|
|
145
|
+
console.error(
|
|
146
|
+
`Failed to save setting 'fetchNews':\n${reason}`
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
await fetchNews();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function fetchNews() {
|
|
159
|
+
if ((settings?.get('fetchNews').composite ?? 'false') === 'true') {
|
|
160
|
+
try {
|
|
161
|
+
const response = await requestAPI<{
|
|
162
|
+
news: (Notification.INotification & { link: [string, string] })[];
|
|
163
|
+
}>(NEWS_API_URL);
|
|
164
|
+
|
|
165
|
+
for (const { link, message, type, options } of response.news) {
|
|
166
|
+
// @ts-expect-error data has no index
|
|
167
|
+
const id = options.data!['id'] as string;
|
|
168
|
+
// Filter those notifications
|
|
169
|
+
const state = (config.data[id] as INewsState) ?? {
|
|
170
|
+
seen: false,
|
|
171
|
+
dismissed: false
|
|
172
|
+
};
|
|
173
|
+
if (!state.dismissed) {
|
|
174
|
+
options.actions = [
|
|
175
|
+
{
|
|
176
|
+
label: trans.__('Hide'),
|
|
177
|
+
caption: trans.__('Never show this notification again.'),
|
|
178
|
+
callback: () => {
|
|
179
|
+
const update: { [k: string]: INewsState } = {};
|
|
180
|
+
update[id] = { seen: true, dismissed: true };
|
|
181
|
+
config.update(update as any).catch(reason => {
|
|
182
|
+
console.error(
|
|
183
|
+
`Failed to update the announcements config:\n${reason}`
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
if (link?.length === 2) {
|
|
190
|
+
options.actions.push({
|
|
191
|
+
label: link[0],
|
|
192
|
+
caption: link[1],
|
|
193
|
+
callback: () => {
|
|
194
|
+
window.open(link[1], '_blank', 'noreferrer');
|
|
195
|
+
},
|
|
196
|
+
displayType: 'link'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (!state.seen) {
|
|
200
|
+
options.autoClose = 5000;
|
|
201
|
+
const update: { [k: string]: INewsState } = {};
|
|
202
|
+
update[id] = { seen: true };
|
|
203
|
+
config.update(update as any).catch(reason => {
|
|
204
|
+
console.error(
|
|
205
|
+
`Failed to update the announcements config:\n${reason}`
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Notification.emit(message, type, options);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (reason) {
|
|
214
|
+
console.log('Failed to get the announcements.', reason);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if ((settings?.get('checkForUpdates').composite as boolean) ?? true) {
|
|
219
|
+
const response = await requestAPI<{
|
|
220
|
+
notification:
|
|
221
|
+
| (Notification.INotification & { link: [string, string] })
|
|
222
|
+
| null;
|
|
223
|
+
}>(UPDATE_API_URL);
|
|
224
|
+
|
|
225
|
+
if (response.notification) {
|
|
226
|
+
const { link, message, type, options } = response.notification;
|
|
227
|
+
// @ts-expect-error data has no index
|
|
228
|
+
const id = options.data!['id'] as string;
|
|
229
|
+
const state = (config.data[id] as INewsState) ?? {
|
|
230
|
+
seen: false,
|
|
231
|
+
dismissed: false
|
|
232
|
+
};
|
|
233
|
+
if (!state.dismissed) {
|
|
234
|
+
let notificationId: string;
|
|
235
|
+
options.actions = [
|
|
236
|
+
{
|
|
237
|
+
label: trans.__('Do not check for updates'),
|
|
238
|
+
caption: trans.__(
|
|
239
|
+
'If pressed, you will not be prompted if a new JupyterLab version is found.'
|
|
240
|
+
),
|
|
241
|
+
callback: () => {
|
|
242
|
+
settings
|
|
243
|
+
?.set('checkForUpdates', false)
|
|
244
|
+
.then(() => {
|
|
245
|
+
Notification.dismiss(notificationId);
|
|
246
|
+
})
|
|
247
|
+
.catch((reason: any) => {
|
|
248
|
+
console.error(
|
|
249
|
+
'Failed to set the `checkForUpdates` setting.',
|
|
250
|
+
reason
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
];
|
|
256
|
+
if (link?.length === 2) {
|
|
257
|
+
options.actions.push({
|
|
258
|
+
label: link[0],
|
|
259
|
+
caption: link[1],
|
|
260
|
+
callback: () => {
|
|
261
|
+
window.open(link[1], '_blank', 'noreferrer');
|
|
262
|
+
},
|
|
263
|
+
displayType: 'link'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (!state.seen) {
|
|
267
|
+
options.autoClose = 5000;
|
|
268
|
+
const update: { [k: string]: INewsState } = {};
|
|
269
|
+
update[id] = { seen: true };
|
|
270
|
+
config.update(update as any).catch(reason => {
|
|
271
|
+
console.error(
|
|
272
|
+
`Failed to update the announcements config:\n${reason}`
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
notificationId = Notification.emit(message, type, options);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* News state
|
|
287
|
+
*/
|
|
288
|
+
interface INewsState {
|
|
289
|
+
/**
|
|
290
|
+
* Whether the news has been seen or not.
|
|
291
|
+
*/
|
|
292
|
+
seen?: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Whether the user has dismissed the news or not.
|
|
295
|
+
*/
|
|
296
|
+
dismissed?: boolean;
|
|
297
|
+
}
|