@oceanum/oceanumlab 4.7.0 → 4.7.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.
Files changed (81) hide show
  1. package/README.md +102 -11
  2. package/lib/DatameshUI.d.ts +1 -6
  3. package/lib/DatameshUI.js +1 -9
  4. package/lib/DatameshWidget.d.ts +18 -2
  5. package/lib/DatameshWidget.js +80 -12
  6. package/lib/StoredNotebooks.d.ts +20 -0
  7. package/lib/StoredNotebooks.js +112 -0
  8. package/lib/Tabs.d.ts +26 -0
  9. package/lib/Tabs.js +45 -0
  10. package/lib/aiBackend.d.ts +1 -1
  11. package/lib/aiBackend.js +1 -1
  12. package/lib/auth/accountWidget.d.ts +35 -0
  13. package/lib/auth/accountWidget.js +95 -0
  14. package/lib/auth/claims.d.ts +3 -0
  15. package/lib/auth/claims.js +25 -0
  16. package/lib/auth/plugin.d.ts +9 -0
  17. package/lib/auth/plugin.js +126 -0
  18. package/lib/auth/serverAuth.d.ts +79 -0
  19. package/lib/auth/serverAuth.js +254 -0
  20. package/lib/auth/startSignIn.d.ts +11 -0
  21. package/lib/auth/startSignIn.js +16 -0
  22. package/lib/auth/tokens.d.ts +102 -0
  23. package/lib/auth/tokens.js +9 -0
  24. package/lib/chatRouter.d.ts +13 -1
  25. package/lib/chatRouter.js +9 -2
  26. package/lib/constants.d.ts +1 -1
  27. package/lib/constants.js +1 -1
  28. package/lib/datameshUiUrl.d.ts +2 -2
  29. package/lib/datameshUiUrl.js +6 -6
  30. package/lib/index.d.ts +8 -1
  31. package/lib/index.js +58 -17
  32. package/lib/share/client.d.ts +84 -0
  33. package/lib/share/client.js +205 -0
  34. package/lib/share/events.d.ts +7 -0
  35. package/lib/share/events.js +9 -0
  36. package/lib/share/notebook.d.ts +184 -0
  37. package/lib/share/notebook.js +277 -0
  38. package/lib/share/oceanumOnly.d.ts +49 -0
  39. package/lib/share/oceanumOnly.js +90 -0
  40. package/lib/share/plugin.d.ts +21 -0
  41. package/lib/share/plugin.js +883 -0
  42. package/lib/share/widgets.d.ts +25 -0
  43. package/lib/share/widgets.js +154 -0
  44. package/package.json +13 -1
  45. package/schema/datamesh-connect.json +3 -12
  46. package/src/DatameshUI.tsx +1 -10
  47. package/src/DatameshWidget.tsx +143 -19
  48. package/src/StoredNotebooks.tsx +206 -0
  49. package/src/Tabs.tsx +102 -0
  50. package/src/__tests__/DatameshWidget.spec.tsx +33 -20
  51. package/src/__tests__/OceanumPanel.spec.tsx +157 -0
  52. package/src/__tests__/StoredNotebooks.spec.tsx +157 -0
  53. package/src/__tests__/Tabs.spec.tsx +168 -0
  54. package/src/__tests__/aiBackend.spec.ts +5 -6
  55. package/src/__tests__/chatRouter.spec.ts +9 -15
  56. package/src/__tests__/datameshUiUrl.spec.ts +4 -3
  57. package/src/__tests__/oceanumOnly.spec.ts +193 -0
  58. package/src/__tests__/pluginArity.spec.ts +24 -0
  59. package/src/__tests__/serverAuth.spec.ts +277 -0
  60. package/src/__tests__/settingsSchema.spec.ts +73 -0
  61. package/src/__tests__/shareClient.spec.ts +314 -0
  62. package/src/__tests__/shareNotebook.spec.ts +451 -0
  63. package/src/__tests__/sharePlugin.spec.ts +426 -0
  64. package/src/aiBackend.ts +1 -1
  65. package/src/auth/accountWidget.ts +113 -0
  66. package/src/auth/claims.ts +30 -0
  67. package/src/auth/plugin.ts +155 -0
  68. package/src/auth/serverAuth.ts +342 -0
  69. package/src/auth/startSignIn.ts +23 -0
  70. package/src/auth/tokens.ts +111 -0
  71. package/src/chatRouter.ts +8 -4
  72. package/src/constants.ts +1 -1
  73. package/src/datameshUiUrl.ts +6 -6
  74. package/src/index.ts +87 -36
  75. package/src/share/client.ts +288 -0
  76. package/src/share/events.ts +10 -0
  77. package/src/share/notebook.ts +416 -0
  78. package/src/share/oceanumOnly.ts +126 -0
  79. package/src/share/plugin.ts +1096 -0
  80. package/src/share/widgets.ts +217 -0
  81. package/style/index.css +266 -13
@@ -0,0 +1,217 @@
1
+ import { Clipboard, Dialog } from '@jupyterlab/apputils';
2
+ import { Widget } from '@lumino/widgets';
3
+
4
+ import type { PermissionGrant, ShareChoice } from './client';
5
+ import { ISpecSummary, parseShareEmails, parseTimestamp } from './notebook';
6
+
7
+ function element<K extends keyof HTMLElementTagNameMap>(
8
+ tag: K,
9
+ className: string,
10
+ text?: string
11
+ ): HTMLElementTagNameMap[K] {
12
+ const node = document.createElement(tag);
13
+ node.className = className;
14
+ if (text !== undefined) {
15
+ // Names and emails come from other users: always set text, never HTML.
16
+ node.textContent = text;
17
+ }
18
+ return node;
19
+ }
20
+
21
+ /** Dialog body listing notebooks to open; its value is the selected record id. */
22
+ export class NotebookListBody
23
+ extends Widget
24
+ implements Dialog.IBodyWidget<string | null>
25
+ {
26
+ constructor(mine: readonly ISpecSummary[], shared: readonly ISpecSummary[]) {
27
+ super();
28
+ this.addClass('jp-OceanumShare-body');
29
+ this._addSection('My notebooks', mine, false);
30
+ this._addSection('Shared with me', shared, true);
31
+ }
32
+
33
+ getValue(): string | null {
34
+ return this._selected;
35
+ }
36
+
37
+ private _addSection(
38
+ title: string,
39
+ items: readonly ISpecSummary[],
40
+ showCreator: boolean
41
+ ): void {
42
+ this.node.appendChild(element('h3', 'jp-OceanumShare-heading', title));
43
+ if (items.length === 0) {
44
+ this.node.appendChild(element('p', 'jp-OceanumShare-empty', 'None'));
45
+ return;
46
+ }
47
+ const list = element('ul', 'jp-OceanumShare-list');
48
+ list.setAttribute('role', 'listbox');
49
+ list.setAttribute('aria-label', title);
50
+ for (const item of items) {
51
+ const option = element('li', 'jp-OceanumShare-item');
52
+ option.setAttribute('role', 'option');
53
+ option.setAttribute('aria-selected', 'false');
54
+ option.tabIndex = 0;
55
+ option.appendChild(element('span', 'jp-OceanumShare-name', item.name));
56
+ const modified = parseTimestamp(item.modified);
57
+ const details = [
58
+ modified ? `Modified ${modified.toLocaleDateString()}` : '',
59
+ showCreator && item.creator ? item.creator : ''
60
+ ].filter(Boolean);
61
+ option.appendChild(
62
+ element('span', 'jp-OceanumShare-meta', details.join(' · '))
63
+ );
64
+ const select = () => this._select(item.id, option);
65
+ option.addEventListener('click', select);
66
+ option.addEventListener('focus', select);
67
+ list.appendChild(option);
68
+ }
69
+ this.node.appendChild(list);
70
+ }
71
+
72
+ private _select(id: string, option: HTMLElement): void {
73
+ this._selected = id;
74
+ for (const node of this.node.querySelectorAll('.jp-OceanumShare-item')) {
75
+ const selected = node === option;
76
+ node.classList.toggle('jp-mod-selected', selected);
77
+ node.setAttribute('aria-selected', String(selected));
78
+ }
79
+ }
80
+
81
+ private _selected: string | null = null;
82
+ }
83
+
84
+ export const EVERYONE_WARNING =
85
+ 'Anyone with the link can read this notebook, including people without an Oceanum ' +
86
+ 'account. This cannot be limited to your organisation. Its name and your email ' +
87
+ 'address also become visible to anyone who lists Oceanum notebooks.';
88
+
89
+ const PUBLIC_GRANT: PermissionGrant = {
90
+ type: 'public',
91
+ entity: '',
92
+ permission: 'read'
93
+ };
94
+
95
+ /** Dialog body for sharing; its value is the change to make, or `null` if incomplete. */
96
+ export class ShareBody
97
+ extends Widget
98
+ implements Dialog.IBodyWidget<ShareChoice | null>
99
+ {
100
+ constructor(link: string) {
101
+ super();
102
+ this.addClass('jp-OceanumShare-body');
103
+
104
+ this.node.appendChild(element('h3', 'jp-OceanumShare-heading', 'Link'));
105
+ const row = element('div', 'jp-OceanumShare-linkRow');
106
+ const linkInput = element('input', 'jp-OceanumShare-link');
107
+ linkInput.readOnly = true;
108
+ linkInput.value = link;
109
+ linkInput.setAttribute('aria-label', 'Notebook link');
110
+ const copy = element(
111
+ 'button',
112
+ 'jp-mod-styled jp-OceanumShare-copy',
113
+ 'Copy link'
114
+ );
115
+ copy.type = 'button';
116
+ copy.addEventListener('click', () => {
117
+ Clipboard.copyToSystem(link);
118
+ copy.textContent = 'Copied';
119
+ });
120
+ row.append(linkInput, copy);
121
+ this.node.appendChild(row);
122
+ this.node.appendChild(
123
+ element(
124
+ 'p',
125
+ 'jp-OceanumShare-hint',
126
+ 'The link opens the notebook for anyone it is shared with.'
127
+ )
128
+ );
129
+
130
+ this.node.appendChild(
131
+ element('h3', 'jp-OceanumShare-heading', 'Share with')
132
+ );
133
+ const name = `jp-OceanumShare-who-${++ShareBody._count}`;
134
+ this._person = this._radio(name, 'A specific person', true);
135
+ const personRow = element('div', 'jp-OceanumShare-personRow');
136
+ this._email = element('textarea', 'jp-OceanumShare-email');
137
+ this._email.rows = 2;
138
+ this._email.placeholder = 'name@example.com, another@example.com';
139
+ this._email.setAttribute('aria-label', 'Email addresses');
140
+ this._level = element('select', 'jp-OceanumShare-level');
141
+ this._level.setAttribute('aria-label', 'Access');
142
+ for (const [value, label] of [
143
+ ['read', 'can view'],
144
+ ['write', 'can edit']
145
+ ]) {
146
+ const option = element('option', '', label);
147
+ option.value = value;
148
+ this._level.appendChild(option);
149
+ }
150
+ personRow.append(this._email, this._level);
151
+ this.node.appendChild(personRow);
152
+ this.node.appendChild(
153
+ element(
154
+ 'p',
155
+ 'jp-OceanumShare-hint',
156
+ // Not "new lines": JupyterLab's dialog swallows Enter before it reaches the
157
+ // textarea, so one cannot be typed here. Pasted ones still separate addresses.
158
+ 'Separate several addresses with commas or spaces. Everyone you add gets the access chosen here.'
159
+ )
160
+ );
161
+
162
+ this._everyone = this._radio(name, 'Everyone with the link', false);
163
+ this.node.appendChild(
164
+ element('p', 'jp-OceanumShare-warning', EVERYONE_WARNING)
165
+ );
166
+ this._stopEveryone = this._radio(name, 'Stop sharing with everyone', false);
167
+
168
+ const update = () => {
169
+ this._email.disabled = !this._person.checked;
170
+ this._level.disabled = !this._person.checked;
171
+ };
172
+ for (const radio of [this._person, this._everyone, this._stopEveryone]) {
173
+ radio.addEventListener('change', update);
174
+ }
175
+ }
176
+
177
+ getValue(): ShareChoice | null {
178
+ if (this._everyone.checked) {
179
+ return { action: 'grant', grants: [PUBLIC_GRANT], invalid: [] };
180
+ }
181
+ if (this._stopEveryone.checked) {
182
+ return { action: 'revoke', grant: PUBLIC_GRANT };
183
+ }
184
+ const { emails, invalid } = parseShareEmails(this._email.value);
185
+ if (emails.length === 0 && invalid.length === 0) {
186
+ return null;
187
+ }
188
+ const permission = this._level.value === 'write' ? 'write' : 'read';
189
+ return {
190
+ action: 'grant',
191
+ grants: emails.map(entity => ({ type: 'user', entity, permission })),
192
+ invalid
193
+ };
194
+ }
195
+
196
+ private _radio(
197
+ name: string,
198
+ text: string,
199
+ checked: boolean
200
+ ): HTMLInputElement {
201
+ const label = element('label', 'jp-OceanumShare-choice');
202
+ const input = element('input', 'jp-OceanumShare-radio');
203
+ input.type = 'radio';
204
+ input.name = name;
205
+ input.checked = checked;
206
+ label.append(input, document.createTextNode(` ${text}`));
207
+ this.node.appendChild(label);
208
+ return input;
209
+ }
210
+
211
+ private static _count = 0;
212
+ private readonly _person: HTMLInputElement;
213
+ private readonly _everyone: HTMLInputElement;
214
+ private readonly _stopEveryone: HTMLInputElement;
215
+ private readonly _email: HTMLTextAreaElement;
216
+ private readonly _level: HTMLSelectElement;
217
+ }
package/style/index.css CHANGED
@@ -43,17 +43,26 @@
43
43
  color: var(--jp-ui-font-color1);
44
44
  }
45
45
 
46
+ /* Headers within the panel's tabs (Datamesh Workspace and the workspace name, Oceanum
47
+ * AI, My notebooks / Shared with me) share one text style: bold, standard case. */
48
+ .datamesh-workspace-header,
49
+ .datamesh-workspace-name,
50
+ .oceanum-ai-chat-header,
51
+ .oceanum-notebooks-heading {
52
+ font-weight: 600;
53
+ font-size: var(--jp-ui-font-size1);
54
+ color: var(--jp-ui-font-color1);
55
+ letter-spacing: 0.5px;
56
+ text-transform: none;
57
+ }
58
+
46
59
  /* Datamesh Workspace section header */
47
60
  .datamesh-workspace-header {
48
61
  display: flex;
49
62
  align-items: center;
50
63
  justify-content: space-between;
51
64
  padding: 8px 12px;
52
- font-weight: 600;
53
- font-size: var(--jp-ui-font-size0);
54
- color: var(--jp-ui-font-color2);
55
65
  background: var(--jp-layout-color1);
56
- letter-spacing: 0.5px;
57
66
  }
58
67
 
59
68
  .add-connection {
@@ -78,13 +87,9 @@
78
87
 
79
88
  .datamesh-workspace-name {
80
89
  flex: 0 0 auto;
81
- font-weight: 600;
82
- text-transform: uppercase;
83
- letter-spacing: 1px;
84
90
  padding: 8px 8px 8px 12px;
85
91
  margin: 0;
86
92
  margin-bottom: 10px;
87
- font-size: var(--jp-ui-font-size1);
88
93
  }
89
94
 
90
95
  .datasource-item-expand,
@@ -224,12 +229,8 @@
224
229
  display: flex;
225
230
  align-items: center;
226
231
  gap: 8px;
227
- font-weight: bold;
228
- font-size: var(--jp-ui-font-size1);
229
232
  padding: 6px 10px;
230
- color: var(--jp-ui-font-color1);
231
233
  background: var(--jp-layout-color2);
232
- letter-spacing: 0.5px;
233
234
  }
234
235
 
235
236
  .oceanum-ai-chat-title {
@@ -257,8 +258,13 @@
257
258
  white-space: nowrap;
258
259
  }
259
260
 
261
+ /* The thread takes only the height it needs, so the input sits under the last message
262
+ * rather than at the foot of the tab with a gap between them. `min-height: 0` is what
263
+ * lets it shrink past its content once the thread is taller than the space there is:
264
+ * without it a flex item refuses to, and the input would be pushed out of the panel. */
260
265
  .oceanum-ai-chat-messages {
261
- flex: 1 1 auto;
266
+ flex: 0 1 auto;
267
+ min-height: 0;
262
268
  overflow-y: auto;
263
269
  padding: 6px 8px;
264
270
  display: flex;
@@ -475,3 +481,250 @@
475
481
  border-top: 1px solid var(--jp-border-color2);
476
482
  margin: 0.5em 0;
477
483
  }
484
+
485
+ /* Oceanum sign-in: the top-bar account control (src/auth/accountWidget.ts). */
486
+ .oc-Account {
487
+ align-self: center;
488
+ margin: 0 8px;
489
+ padding: 2px 10px;
490
+ border: var(--jp-border-width) solid var(--jp-border-color1);
491
+ border-radius: 12px;
492
+ background: var(--jp-layout-color1);
493
+ color: var(--jp-ui-font-color1);
494
+ font-family: var(--jp-ui-font-family);
495
+ font-size: var(--jp-ui-font-size1);
496
+ white-space: nowrap;
497
+ cursor: pointer;
498
+ }
499
+
500
+ .oc-Account:hover {
501
+ background: var(--jp-layout-color2);
502
+ }
503
+
504
+ .oc-Account:not(.oc-mod-signedIn) {
505
+ border-color: var(--jp-brand-color1);
506
+ color: var(--jp-brand-color1);
507
+ }
508
+
509
+ /* The sign-in dialog: the device code, and the link that confirms it. */
510
+ .oc-SignIn {
511
+ max-width: 360px;
512
+ }
513
+
514
+ .oc-SignIn-code {
515
+ margin: 12px 0;
516
+ padding: 10px;
517
+ border: var(--jp-border-width) solid var(--jp-border-color1);
518
+ border-radius: 4px;
519
+ background: var(--jp-layout-color2);
520
+ font-family: var(--jp-code-font-family);
521
+ font-size: 22px;
522
+ letter-spacing: 3px;
523
+ text-align: center;
524
+ user-select: all;
525
+ }
526
+
527
+ .oc-SignIn-link {
528
+ display: block;
529
+ padding: 6px 12px;
530
+ border-radius: 2px;
531
+ text-align: center;
532
+ text-decoration: none;
533
+ }
534
+
535
+ .oc-SignIn-note {
536
+ margin-top: 12px;
537
+ color: var(--jp-ui-font-color2);
538
+ font-size: var(--jp-ui-font-size0);
539
+ }
540
+
541
+ /* The Oceanum side panel's tab strip (src/Tabs.tsx). */
542
+ .oceanum-tabs {
543
+ display: flex;
544
+ flex-direction: column;
545
+ min-height: 0;
546
+ flex: 1 1 auto;
547
+ }
548
+
549
+ .oceanum-tabs-strip {
550
+ display: flex;
551
+ border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
552
+ }
553
+
554
+ .oceanum-tab {
555
+ flex: 1 1 auto;
556
+ padding: 6px 4px;
557
+ border: none;
558
+ border-bottom: 2px solid transparent;
559
+ background: transparent;
560
+ color: var(--jp-ui-font-color2);
561
+ font-family: var(--jp-ui-font-family);
562
+ font-size: var(--jp-ui-font-size1);
563
+ cursor: pointer;
564
+ }
565
+
566
+ .oceanum-tab:hover {
567
+ background: var(--jp-layout-color2);
568
+ }
569
+
570
+ .oceanum-tab-selected {
571
+ border-bottom-color: var(--jp-brand-color1);
572
+ color: var(--jp-ui-font-color1);
573
+ }
574
+
575
+ .oceanum-tabpanel {
576
+ display: flex;
577
+ flex-direction: column;
578
+ min-height: 0;
579
+ flex: 1 1 auto;
580
+ overflow: auto;
581
+ }
582
+
583
+ /* Must come after the rule above: an author `display` beats the user agent's
584
+ * `[hidden] { display: none }`, so without this every pane renders at once and the
585
+ * tabs only move the underline. */
586
+ .oceanum-tabpanel[hidden] {
587
+ display: none;
588
+ }
589
+
590
+ /* Stored notebooks (src/StoredNotebooks.tsx). */
591
+ .oceanum-notebooks-heading {
592
+ display: flex;
593
+ justify-content: space-between;
594
+ padding: 6px 8px 2px;
595
+ }
596
+
597
+ .oceanum-notebooks-list {
598
+ margin: 0;
599
+ padding: 0;
600
+ list-style: none;
601
+ }
602
+
603
+ .oceanum-notebooks-item {
604
+ display: block;
605
+ width: 100%;
606
+ padding: 4px 8px;
607
+ border: none;
608
+ background: transparent;
609
+ color: var(--jp-ui-font-color1);
610
+ font-family: var(--jp-ui-font-family);
611
+ font-size: var(--jp-ui-font-size1);
612
+ text-align: left;
613
+ cursor: pointer;
614
+ }
615
+
616
+ .oceanum-notebooks-item:hover {
617
+ background: var(--jp-layout-color2);
618
+ }
619
+
620
+ .oceanum-notebooks-item-static {
621
+ cursor: default;
622
+ }
623
+
624
+ .oceanum-notebooks-item-static:hover {
625
+ background: transparent;
626
+ }
627
+
628
+ .oceanum-notebooks-name {
629
+ overflow: hidden;
630
+ text-overflow: ellipsis;
631
+ white-space: nowrap;
632
+ }
633
+
634
+ .oceanum-text-error {
635
+ padding: 8px;
636
+ color: var(--jp-error-color1);
637
+ font-size: var(--jp-ui-font-size1);
638
+ }
639
+
640
+ /* Open from Oceanum and Share on Oceanum dialogs (src/share/widgets.ts). */
641
+ .jp-OceanumShare-body {
642
+ min-width: 320px;
643
+ max-width: 560px;
644
+ }
645
+
646
+ .jp-OceanumShare-heading {
647
+ margin: 12px 0 6px;
648
+ font-size: var(--jp-ui-font-size1);
649
+ font-weight: 600;
650
+ }
651
+
652
+ .jp-OceanumShare-heading:first-child {
653
+ margin-top: 0;
654
+ }
655
+
656
+ .jp-OceanumShare-empty,
657
+ .jp-OceanumShare-hint {
658
+ margin: 0 0 6px;
659
+ color: var(--jp-ui-font-color2);
660
+ }
661
+
662
+ .jp-OceanumShare-list {
663
+ max-height: 240px;
664
+ margin: 0;
665
+ padding: 0;
666
+ overflow-y: auto;
667
+ list-style: none;
668
+ border: var(--jp-border-width) solid var(--jp-border-color1);
669
+ }
670
+
671
+ .jp-OceanumShare-item {
672
+ display: flex;
673
+ flex-direction: column;
674
+ padding: 6px 8px;
675
+ cursor: pointer;
676
+ }
677
+
678
+ .jp-OceanumShare-item:hover {
679
+ background: var(--jp-layout-color2);
680
+ }
681
+
682
+ .jp-OceanumShare-item.jp-mod-selected {
683
+ background: var(--jp-brand-color3);
684
+ }
685
+
686
+ .jp-OceanumShare-meta {
687
+ color: var(--jp-ui-font-color2);
688
+ font-size: var(--jp-ui-font-size0);
689
+ }
690
+
691
+ .jp-OceanumShare-linkRow,
692
+ .jp-OceanumShare-personRow {
693
+ display: flex;
694
+ gap: 6px;
695
+ align-items: center;
696
+ margin-bottom: 6px;
697
+ }
698
+
699
+ .jp-OceanumShare-linkRow > :first-child,
700
+ .jp-OceanumShare-personRow > :first-child {
701
+ flex: 1;
702
+ }
703
+
704
+ /* A textarea so several addresses fit; without this it would default to monospace. */
705
+ .jp-OceanumShare-email {
706
+ font: inherit;
707
+ resize: vertical;
708
+ }
709
+
710
+ /* Keep the access select beside the first line of addresses, not centred against them. */
711
+ .jp-OceanumShare-personRow {
712
+ align-items: flex-start;
713
+ }
714
+
715
+ .jp-OceanumShare-choice {
716
+ display: block;
717
+ margin: 6px 0;
718
+ }
719
+
720
+ .jp-OceanumShare-choice input[type='radio'] {
721
+ width: auto;
722
+ height: auto;
723
+ margin: 0 4px 0 0;
724
+ appearance: auto;
725
+ }
726
+
727
+ .jp-OceanumShare-warning {
728
+ margin: 0 0 0 20px;
729
+ color: var(--jp-warn-color0);
730
+ }