@oceanum/oceanumlab 4.7.0 → 4.7.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.
- package/README.md +102 -11
- package/lib/DatameshUI.d.ts +1 -6
- package/lib/DatameshUI.js +1 -9
- package/lib/DatameshWidget.d.ts +18 -2
- package/lib/DatameshWidget.js +70 -11
- package/lib/StoredNotebooks.d.ts +16 -0
- package/lib/StoredNotebooks.js +84 -0
- package/lib/Tabs.d.ts +26 -0
- package/lib/Tabs.js +45 -0
- package/lib/aiBackend.d.ts +1 -1
- package/lib/aiBackend.js +1 -1
- package/lib/auth/accountWidget.d.ts +35 -0
- package/lib/auth/accountWidget.js +95 -0
- package/lib/auth/claims.d.ts +3 -0
- package/lib/auth/claims.js +25 -0
- package/lib/auth/plugin.d.ts +9 -0
- package/lib/auth/plugin.js +126 -0
- package/lib/auth/serverAuth.d.ts +79 -0
- package/lib/auth/serverAuth.js +253 -0
- package/lib/auth/tokens.d.ts +95 -0
- package/lib/auth/tokens.js +9 -0
- package/lib/chatRouter.d.ts +13 -1
- package/lib/chatRouter.js +9 -2
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +2 -2
- package/lib/datameshUiUrl.js +6 -6
- package/lib/index.d.ts +8 -1
- package/lib/index.js +56 -17
- package/lib/specStore.d.ts +37 -0
- package/lib/specStore.js +57 -0
- package/package.json +9 -1
- package/schema/datamesh-connect.json +3 -12
- package/src/DatameshUI.tsx +1 -10
- package/src/DatameshWidget.tsx +124 -19
- package/src/StoredNotebooks.tsx +166 -0
- package/src/Tabs.tsx +102 -0
- package/src/__tests__/DatameshWidget.spec.tsx +33 -20
- package/src/__tests__/OceanumPanel.spec.tsx +101 -0
- package/src/__tests__/Tabs.spec.tsx +168 -0
- package/src/__tests__/aiBackend.spec.ts +5 -6
- package/src/__tests__/chatRouter.spec.ts +9 -15
- package/src/__tests__/datameshUiUrl.spec.ts +4 -3
- package/src/__tests__/pluginArity.spec.ts +24 -0
- package/src/__tests__/serverAuth.spec.ts +277 -0
- package/src/__tests__/settingsSchema.spec.ts +73 -0
- package/src/__tests__/specStore.spec.ts +126 -0
- package/src/aiBackend.ts +1 -1
- package/src/auth/accountWidget.ts +113 -0
- package/src/auth/claims.ts +30 -0
- package/src/auth/plugin.ts +155 -0
- package/src/auth/serverAuth.ts +341 -0
- package/src/auth/tokens.ts +104 -0
- package/src/chatRouter.ts +8 -4
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +6 -6
- package/src/index.ts +85 -36
- package/src/specStore.ts +82 -0
- package/style/index.css +158 -0
package/src/index.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { requestAPI } from './handler';
|
|
|
18
18
|
import { ChatRouter, ChatRouterError, ChatMessage } from './chatRouter';
|
|
19
19
|
import { reporterFor } from './progress';
|
|
20
20
|
import { ConversationPin } from './conversationPin';
|
|
21
|
+
import { deviceAuthPlugin } from './auth/plugin';
|
|
22
|
+
import { IOceanumAuth } from './auth/tokens';
|
|
23
|
+
|
|
21
24
|
import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
22
25
|
import { notebookHost } from './notebookHost';
|
|
23
26
|
import { KernelHandoff } from './kernelHandoff';
|
|
@@ -53,20 +56,27 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
53
56
|
ISettingRegistry,
|
|
54
57
|
IStateDB
|
|
55
58
|
],
|
|
59
|
+
// Optional so the panel still loads on a host with no Oceanum environment; the
|
|
60
|
+
// Notebooks tab then explains why it is empty instead of the panel failing to start.
|
|
61
|
+
optional: [IOceanumAuth],
|
|
56
62
|
activate: (
|
|
57
63
|
app: JupyterFrontEnd,
|
|
58
64
|
palette: ICommandPalette,
|
|
59
65
|
restorer: ILayoutRestorer,
|
|
60
66
|
status: ILabStatus,
|
|
61
|
-
settingRegistry: ISettingRegistry
|
|
67
|
+
settingRegistry: ISettingRegistry,
|
|
68
|
+
stateDB: IStateDB,
|
|
69
|
+
auth: IOceanumAuth | null
|
|
62
70
|
) => {
|
|
63
71
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
64
72
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
73
|
+
// Where the Datamesh UI and Oceanum AI live is the deployment's business, so it comes
|
|
74
|
+
// from the sign-in environment (the Jupyter server's configuration, or the notebook
|
|
75
|
+
// site's) and falls back to production. These were user settings until 4.7.1; a
|
|
76
|
+
// user-editable service address is somewhere to send the user's credential.
|
|
77
|
+
const datameshUiUrl = auth?.urls?.datameshUi ?? DATAMESH_UI_SERVICE;
|
|
78
|
+
const aiBackendUrl = auth?.urls?.ai ?? OCEANUM_AI_BACKEND_URL;
|
|
79
|
+
// The `datameshToken` setting for the AI chat; empty until the settings load.
|
|
70
80
|
let datameshTokenSetting = '';
|
|
71
81
|
// Tells the sidebar the settings above have changed.
|
|
72
82
|
const settingsChanged = new Signal<JupyterFrontEnd, void>(app);
|
|
@@ -88,14 +98,7 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
88
98
|
}).then(res => console.log(res));
|
|
89
99
|
}
|
|
90
100
|
window.injectToken = set.get('injectToken').user as boolean;
|
|
91
|
-
aiBackendUrl = set.get('aiBackendUrl').composite as string;
|
|
92
101
|
datameshTokenSetting = set.get('datameshToken').composite as string;
|
|
93
|
-
// An open Datamesh UI panel follows a change of address.
|
|
94
|
-
datameshUiUrl = set.get('datameshUiUrl').composite as string;
|
|
95
|
-
const datameshUI = findDatameshUI();
|
|
96
|
-
if (datameshUI) {
|
|
97
|
-
datameshUI.url = datameshUiUrl;
|
|
98
|
-
}
|
|
99
102
|
settingsChanged.emit();
|
|
100
103
|
};
|
|
101
104
|
//Try to get the datamesh token from the envars
|
|
@@ -145,7 +148,13 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
145
148
|
aiBackendUrl: () => aiBackendUrl,
|
|
146
149
|
settingsChanged,
|
|
147
150
|
commands: app.commands,
|
|
148
|
-
getCurrentWidget
|
|
151
|
+
getCurrentWidget,
|
|
152
|
+
auth
|
|
153
|
+
// No `openStoredNotebook`: nothing can open a spec store record by id yet.
|
|
154
|
+
// share-oceanum has `oceanum-share:open`, but it takes no arguments — it opens
|
|
155
|
+
// its own picker — so there is nothing to hand an id to. Opening arrives with
|
|
156
|
+
// the drive in OCE-182, where the contents API makes it the ordinary path.
|
|
157
|
+
// Until then the list reads rather than offering a click that does nothing.
|
|
149
158
|
});
|
|
150
159
|
datameshConnectWidget.id = 'datamesh-connect';
|
|
151
160
|
datameshConnectWidget.title.icon = oceanumIcon;
|
|
@@ -153,8 +162,30 @@ export const datamesh_connect_extension: JupyterFrontEndPlugin<void> = {
|
|
|
153
162
|
|
|
154
163
|
restorer.add(datameshConnectWidget, 'datamesh-connect');
|
|
155
164
|
|
|
165
|
+
// The layout restorer restores the widget's place in the shell, not fields on it,
|
|
166
|
+
// so the chosen tab is kept here. Restoring is best effort: a missing or unreadable
|
|
167
|
+
// entry just leaves the default.
|
|
168
|
+
const TAB_STATE_KEY = `${PLUGIN_ID}:tab`;
|
|
169
|
+
void stateDB
|
|
170
|
+
.fetch(TAB_STATE_KEY)
|
|
171
|
+
.then(value => {
|
|
172
|
+
if (typeof value === 'string') {
|
|
173
|
+
datameshConnectWidget.selectTab(value);
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
.catch((): void => undefined);
|
|
177
|
+
datameshConnectWidget.tabChanged.connect((_, id) => {
|
|
178
|
+
void stateDB.save(TAB_STATE_KEY, id);
|
|
179
|
+
});
|
|
180
|
+
|
|
156
181
|
// Rank has been chosen somewhat arbitrarily to give priority to the running
|
|
157
182
|
// sessions widget in the sidebar.
|
|
183
|
+
//
|
|
184
|
+
// Oceanum Notebook wants this panel first instead, but that is a property of that
|
|
185
|
+
// distribution rather than of the extension: in a plain JupyterLab the file browser
|
|
186
|
+
// is the primary navigation surface and displacing it would be a regression. A
|
|
187
|
+
// distribution moves it with JupyterLab's own shell user-layout settings, keyed on
|
|
188
|
+
// this widget's id, rather than this extension hard-coding one host's preference.
|
|
158
189
|
app.shell.add(datameshConnectWidget, 'left', { rank: 900 });
|
|
159
190
|
|
|
160
191
|
app.commands.addCommand('datamesh-ui:open', {
|
|
@@ -174,10 +205,12 @@ export const oceanum_ai_extension: JupyterFrontEndPlugin<void> = {
|
|
|
174
205
|
id: '@oceanum/oceanumlab:ai-chat',
|
|
175
206
|
autoStart: true,
|
|
176
207
|
requires: [INotebookTracker, ISettingRegistry],
|
|
208
|
+
optional: [IOceanumAuth],
|
|
177
209
|
activate: (
|
|
178
210
|
app: JupyterFrontEnd,
|
|
179
211
|
notebookTracker: INotebookTracker,
|
|
180
|
-
settingRegistry: ISettingRegistry
|
|
212
|
+
settingRegistry: ISettingRegistry,
|
|
213
|
+
auth: IOceanumAuth | null
|
|
181
214
|
) => {
|
|
182
215
|
console.log('Oceanum AI chat extension is loaded');
|
|
183
216
|
|
|
@@ -197,26 +230,31 @@ export const oceanum_ai_extension: JupyterFrontEndPlugin<void> = {
|
|
|
197
230
|
// The panel the current request read its selected cell from, if the
|
|
198
231
|
// notebook was open: a code answer may replace that cell only there.
|
|
199
232
|
let readFrom: NotebookPanel | null = null;
|
|
200
|
-
const router = new ChatRouter(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
const router = new ChatRouter(
|
|
234
|
+
settings,
|
|
235
|
+
app.commands,
|
|
236
|
+
async () => {
|
|
237
|
+
const open = await pin.current();
|
|
238
|
+
readFrom = open;
|
|
239
|
+
if (open) {
|
|
240
|
+
return snapshotOf(open.content);
|
|
241
|
+
}
|
|
242
|
+
const path = pin.path();
|
|
243
|
+
if (!path) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const file = await app.serviceManager.contents.get(path, {
|
|
248
|
+
content: true
|
|
249
|
+
});
|
|
250
|
+
return snapshotFromIpynb(file.content);
|
|
251
|
+
} catch {
|
|
252
|
+
// Gone: an answer's cells will go into a new notebook instead.
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
() => auth?.urls?.ai ?? ''
|
|
257
|
+
);
|
|
220
258
|
// Asked only when an answer has blocks to place: brings the
|
|
221
259
|
// conversation's notebook to the front, opening it again if closed.
|
|
222
260
|
const handoff = new KernelHandoff(
|
|
@@ -352,4 +390,15 @@ export const oceanum_ai_extension: JupyterFrontEndPlugin<void> = {
|
|
|
352
390
|
}
|
|
353
391
|
};
|
|
354
392
|
|
|
355
|
-
|
|
393
|
+
/**
|
|
394
|
+
* The sign-in contract, exported so another extension can provide or consume it: a Lumino
|
|
395
|
+
* token is matched by object identity, so everyone has to import this one object, and
|
|
396
|
+
* JupyterLab shares `@oceanum/oceanumlab` between extensions as a singleton module.
|
|
397
|
+
*/
|
|
398
|
+
export * from './auth/tokens';
|
|
399
|
+
|
|
400
|
+
export default [
|
|
401
|
+
deviceAuthPlugin,
|
|
402
|
+
datamesh_connect_extension,
|
|
403
|
+
oceanum_ai_extension
|
|
404
|
+
];
|
package/src/specStore.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the Oceanum spec store's `notebook` records.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
|
|
5
|
+
* share-oceanum package has a fuller client (create, update, permissions), and when
|
|
6
|
+
* sharing moves here the two should become one rather than sitting side by side.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A spec store list entry. The list omits the notebook body. */
|
|
10
|
+
export interface ISpecSummary {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string | null;
|
|
14
|
+
/** Set when the record is created and never updated, so this is a creation time. */
|
|
15
|
+
modified: string;
|
|
16
|
+
/** Null unless the caller created the record: the store hides other creators. */
|
|
17
|
+
creator: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isSummary(value: unknown): value is ISpecSummary {
|
|
21
|
+
if (typeof value !== 'object' || value === null) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const v = value as Record<string, unknown>;
|
|
25
|
+
return (
|
|
26
|
+
typeof v.id === 'string' &&
|
|
27
|
+
typeof v.name === 'string' &&
|
|
28
|
+
typeof v.modified === 'string'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every notebook the signed-in user may read: their own and those shared with them.
|
|
34
|
+
*
|
|
35
|
+
* The store answers an unauthenticated caller with an empty list rather than an error,
|
|
36
|
+
* so a missing token is indistinguishable from an empty account — callers should check
|
|
37
|
+
* they are signed in before showing "no notebooks".
|
|
38
|
+
*/
|
|
39
|
+
export async function listNotebooks(
|
|
40
|
+
specsUrl: string,
|
|
41
|
+
token: string,
|
|
42
|
+
fetcher: typeof fetch = fetch
|
|
43
|
+
): Promise<ISpecSummary[]> {
|
|
44
|
+
const url = `${specsUrl.replace(/\/+$/, '')}/specs/notebook`;
|
|
45
|
+
const response = await fetcher(url, {
|
|
46
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
// The store answers a bad or expired token with 400, and 403 both for no access
|
|
50
|
+
// and for a record that does not exist.
|
|
51
|
+
throw new Error(`Oceanum.io returned HTTP ${response.status}.`);
|
|
52
|
+
}
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
if (!Array.isArray(data)) {
|
|
55
|
+
throw new Error('Oceanum.io returned an unexpected response.');
|
|
56
|
+
}
|
|
57
|
+
return data.filter(isSummary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Split a listing into the caller's own notebooks and the rest, newest first.
|
|
62
|
+
*
|
|
63
|
+
* The store only sends `creator` on records the caller created, so anything else it
|
|
64
|
+
* returns is shared with them — directly, or because it is public. Addresses are
|
|
65
|
+
* compared case-insensitively: the store matches entities as case-sensitive globs, so
|
|
66
|
+
* the two sides can differ in case.
|
|
67
|
+
*/
|
|
68
|
+
export function partitionNotebooks(
|
|
69
|
+
items: readonly ISpecSummary[],
|
|
70
|
+
email: string | null
|
|
71
|
+
): { mine: ISpecSummary[]; shared: ISpecSummary[] } {
|
|
72
|
+
const me = email?.toLowerCase() ?? null;
|
|
73
|
+
const sorted = [...items].sort((a, b) =>
|
|
74
|
+
b.modified.localeCompare(a.modified)
|
|
75
|
+
);
|
|
76
|
+
const isMine = (item: ISpecSummary): boolean =>
|
|
77
|
+
me !== null && item.creator?.toLowerCase() === me;
|
|
78
|
+
return {
|
|
79
|
+
mine: sorted.filter(isMine),
|
|
80
|
+
shared: sorted.filter(item => !isMine(item))
|
|
81
|
+
};
|
|
82
|
+
}
|
package/style/index.css
CHANGED
|
@@ -475,3 +475,161 @@
|
|
|
475
475
|
border-top: 1px solid var(--jp-border-color2);
|
|
476
476
|
margin: 0.5em 0;
|
|
477
477
|
}
|
|
478
|
+
|
|
479
|
+
/* Oceanum sign-in: the top-bar account control (src/auth/accountWidget.ts). */
|
|
480
|
+
.oc-Account {
|
|
481
|
+
align-self: center;
|
|
482
|
+
margin: 0 8px;
|
|
483
|
+
padding: 2px 10px;
|
|
484
|
+
border: var(--jp-border-width) solid var(--jp-border-color1);
|
|
485
|
+
border-radius: 12px;
|
|
486
|
+
background: var(--jp-layout-color1);
|
|
487
|
+
color: var(--jp-ui-font-color1);
|
|
488
|
+
font-family: var(--jp-ui-font-family);
|
|
489
|
+
font-size: var(--jp-ui-font-size1);
|
|
490
|
+
white-space: nowrap;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.oc-Account:hover {
|
|
495
|
+
background: var(--jp-layout-color2);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.oc-Account:not(.oc-mod-signedIn) {
|
|
499
|
+
border-color: var(--jp-brand-color1);
|
|
500
|
+
color: var(--jp-brand-color1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* The sign-in dialog: the device code, and the link that confirms it. */
|
|
504
|
+
.oc-SignIn {
|
|
505
|
+
max-width: 360px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.oc-SignIn-code {
|
|
509
|
+
margin: 12px 0;
|
|
510
|
+
padding: 10px;
|
|
511
|
+
border: var(--jp-border-width) solid var(--jp-border-color1);
|
|
512
|
+
border-radius: 4px;
|
|
513
|
+
background: var(--jp-layout-color2);
|
|
514
|
+
font-family: var(--jp-code-font-family);
|
|
515
|
+
font-size: 22px;
|
|
516
|
+
letter-spacing: 3px;
|
|
517
|
+
text-align: center;
|
|
518
|
+
user-select: all;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.oc-SignIn-link {
|
|
522
|
+
display: block;
|
|
523
|
+
padding: 6px 12px;
|
|
524
|
+
border-radius: 2px;
|
|
525
|
+
text-align: center;
|
|
526
|
+
text-decoration: none;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.oc-SignIn-note {
|
|
530
|
+
margin-top: 12px;
|
|
531
|
+
color: var(--jp-ui-font-color2);
|
|
532
|
+
font-size: var(--jp-ui-font-size0);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* The Oceanum side panel's tab strip (src/Tabs.tsx). */
|
|
536
|
+
.oceanum-tabs {
|
|
537
|
+
display: flex;
|
|
538
|
+
flex-direction: column;
|
|
539
|
+
min-height: 0;
|
|
540
|
+
flex: 1 1 auto;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.oceanum-tabs-strip {
|
|
544
|
+
display: flex;
|
|
545
|
+
border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.oceanum-tab {
|
|
549
|
+
flex: 1 1 auto;
|
|
550
|
+
padding: 6px 4px;
|
|
551
|
+
border: none;
|
|
552
|
+
border-bottom: 2px solid transparent;
|
|
553
|
+
background: transparent;
|
|
554
|
+
color: var(--jp-ui-font-color2);
|
|
555
|
+
font-family: var(--jp-ui-font-family);
|
|
556
|
+
font-size: var(--jp-ui-font-size1);
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.oceanum-tab:hover {
|
|
561
|
+
background: var(--jp-layout-color2);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.oceanum-tab-selected {
|
|
565
|
+
border-bottom-color: var(--jp-brand-color1);
|
|
566
|
+
color: var(--jp-ui-font-color1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.oceanum-tabpanel {
|
|
570
|
+
display: flex;
|
|
571
|
+
flex-direction: column;
|
|
572
|
+
min-height: 0;
|
|
573
|
+
flex: 1 1 auto;
|
|
574
|
+
overflow: auto;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Must come after the rule above: an author `display` beats the user agent's
|
|
578
|
+
* `[hidden] { display: none }`, so without this every pane renders at once and the
|
|
579
|
+
* tabs only move the underline. */
|
|
580
|
+
.oceanum-tabpanel[hidden] {
|
|
581
|
+
display: none;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Stored notebooks (src/StoredNotebooks.tsx). */
|
|
585
|
+
.oceanum-notebooks-heading {
|
|
586
|
+
display: flex;
|
|
587
|
+
justify-content: space-between;
|
|
588
|
+
padding: 6px 8px 2px;
|
|
589
|
+
color: var(--jp-ui-font-color2);
|
|
590
|
+
font-size: var(--jp-ui-font-size0);
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.oceanum-notebooks-list {
|
|
595
|
+
margin: 0;
|
|
596
|
+
padding: 0;
|
|
597
|
+
list-style: none;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.oceanum-notebooks-item {
|
|
601
|
+
display: block;
|
|
602
|
+
width: 100%;
|
|
603
|
+
padding: 4px 8px;
|
|
604
|
+
border: none;
|
|
605
|
+
background: transparent;
|
|
606
|
+
color: var(--jp-ui-font-color1);
|
|
607
|
+
font-family: var(--jp-ui-font-family);
|
|
608
|
+
font-size: var(--jp-ui-font-size1);
|
|
609
|
+
text-align: left;
|
|
610
|
+
cursor: pointer;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.oceanum-notebooks-item:hover {
|
|
614
|
+
background: var(--jp-layout-color2);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.oceanum-notebooks-item-static {
|
|
618
|
+
cursor: default;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.oceanum-notebooks-item-static:hover {
|
|
622
|
+
background: transparent;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.oceanum-notebooks-name {
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
text-overflow: ellipsis;
|
|
628
|
+
white-space: nowrap;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.oceanum-text-error {
|
|
632
|
+
padding: 8px;
|
|
633
|
+
color: var(--jp-error-color1);
|
|
634
|
+
font-size: var(--jp-ui-font-size1);
|
|
635
|
+
}
|