@jupyterlab/apputils-extension 4.0.0-alpha.9 → 4.0.0-beta.1

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.
Files changed (48) hide show
  1. package/lib/announcements.d.ts +2 -0
  2. package/lib/announcements.js +227 -0
  3. package/lib/announcements.js.map +1 -0
  4. package/lib/index.js +66 -12
  5. package/lib/index.js.map +1 -1
  6. package/lib/notificationplugin.d.ts +5 -0
  7. package/lib/notificationplugin.js +504 -0
  8. package/lib/notificationplugin.js.map +1 -0
  9. package/lib/palette.js +3 -3
  10. package/lib/palette.js.map +1 -1
  11. package/lib/settingconnector.d.ts +4 -1
  12. package/lib/settingconnector.js +8 -1
  13. package/lib/settingconnector.js.map +1 -1
  14. package/lib/settingsplugin.js +6 -5
  15. package/lib/settingsplugin.js.map +1 -1
  16. package/lib/shortcuts.d.ts +21 -0
  17. package/lib/shortcuts.js +137 -0
  18. package/lib/shortcuts.js.map +1 -0
  19. package/lib/statusbarplugin.js +10 -9
  20. package/lib/statusbarplugin.js.map +1 -1
  21. package/lib/themesplugins.js +24 -0
  22. package/lib/themesplugins.js.map +1 -1
  23. package/lib/toolbarregistryplugin.js +4 -0
  24. package/lib/toolbarregistryplugin.js.map +1 -1
  25. package/lib/workspacesplugin.js +6 -6
  26. package/lib/workspacesplugin.js.map +1 -1
  27. package/package.json +28 -23
  28. package/schema/notification.json +48 -0
  29. package/schema/palette.json +2 -0
  30. package/schema/sanitizer.json +25 -0
  31. package/schema/utilityCommands.json +35 -0
  32. package/src/announcements.ts +297 -0
  33. package/src/index.ts +721 -0
  34. package/src/notificationplugin.tsx +902 -0
  35. package/src/palette.ts +213 -0
  36. package/src/settingconnector.ts +73 -0
  37. package/src/settingsplugin.ts +66 -0
  38. package/src/shortcuts.tsx +191 -0
  39. package/src/statusbarplugin.ts +145 -0
  40. package/src/themesplugins.ts +290 -0
  41. package/src/toolbarregistryplugin.ts +29 -0
  42. package/src/typings.d.ts +9 -0
  43. package/src/workspacesplugin.ts +306 -0
  44. package/style/base.css +2 -1
  45. package/style/contextualhelp.css +42 -0
  46. package/style/notification.css +227 -0
  47. package/style/scrollbar.raw.css +64 -0
  48. 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
+ }