@oceanum/oceanumlab 4.7.1 → 4.8.0
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/DatameshWidget.d.ts +11 -0
- package/lib/DatameshWidget.js +35 -5
- package/lib/StoredNotebooks.d.ts +27 -5
- package/lib/StoredNotebooks.js +141 -24
- package/lib/auth/serverAuth.js +1 -0
- package/lib/auth/startSignIn.d.ts +11 -0
- package/lib/auth/startSignIn.js +16 -0
- package/lib/auth/tokens.d.ts +13 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +43 -7
- package/lib/share/client.d.ts +86 -0
- package/lib/share/client.js +206 -0
- package/lib/share/events.d.ts +7 -0
- package/lib/share/events.js +9 -0
- package/lib/share/notebook.d.ts +216 -0
- package/lib/share/notebook.js +355 -0
- package/lib/share/oceanumOnly.d.ts +72 -0
- package/lib/share/oceanumOnly.js +127 -0
- package/lib/share/plugin.d.ts +23 -0
- package/lib/share/plugin.js +981 -0
- package/lib/share/upload.d.ts +6 -0
- package/lib/share/upload.js +27 -0
- package/lib/share/widgets.d.ts +25 -0
- package/lib/share/widgets.js +154 -0
- package/package.json +5 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshWidget.tsx +79 -2
- package/src/StoredNotebooks.tsx +274 -24
- package/src/__tests__/OceanumPanel.spec.tsx +159 -5
- package/src/__tests__/StoredNotebooks.spec.tsx +528 -0
- package/src/__tests__/oceanumOnly.spec.ts +317 -0
- package/src/__tests__/settingsSchema.spec.ts +11 -1
- package/src/__tests__/shareClient.spec.ts +328 -0
- package/src/__tests__/shareNotebook.spec.ts +624 -0
- package/src/__tests__/sharePlugin.spec.ts +664 -0
- package/src/auth/serverAuth.ts +1 -0
- package/src/auth/startSignIn.ts +23 -0
- package/src/auth/tokens.ts +13 -0
- package/src/index.ts +43 -7
- package/src/share/client.ts +291 -0
- package/src/share/events.ts +10 -0
- package/src/share/notebook.ts +525 -0
- package/src/share/oceanumOnly.ts +177 -0
- package/src/share/plugin.ts +1220 -0
- package/src/share/upload.ts +27 -0
- package/src/share/widgets.ts +217 -0
- package/style/index.css +212 -18
- package/lib/specStore.d.ts +0 -37
- package/lib/specStore.js +0 -57
- package/src/__tests__/specStore.spec.ts +0 -126
- package/src/specStore.ts +0 -82
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { ISpecSummary, listNotebooks, partitionNotebooks } from '../specStore';
|
|
2
|
-
|
|
3
|
-
function summary(overrides: Partial<ISpecSummary> = {}): ISpecSummary {
|
|
4
|
-
return {
|
|
5
|
-
id: 'nb-1',
|
|
6
|
-
name: 'Wave analysis',
|
|
7
|
-
description: null,
|
|
8
|
-
modified: '2026-05-17T12:00:00',
|
|
9
|
-
creator: null,
|
|
10
|
-
...overrides
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function response(body: unknown, status = 200): Response {
|
|
15
|
-
return {
|
|
16
|
-
ok: status >= 200 && status < 300,
|
|
17
|
-
status,
|
|
18
|
-
json: async () => body
|
|
19
|
-
} as unknown as Response;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('listNotebooks', () => {
|
|
23
|
-
it('asks the notebook spec type with the token as a bearer credential', async () => {
|
|
24
|
-
const calls: [string, RequestInit | undefined][] = [];
|
|
25
|
-
const fetcher = (async (url: string, init?: RequestInit) => {
|
|
26
|
-
calls.push([url, init]);
|
|
27
|
-
return response([]);
|
|
28
|
-
}) as unknown as typeof fetch;
|
|
29
|
-
|
|
30
|
-
await listNotebooks('https://specs.example.com', 'jwt', fetcher);
|
|
31
|
-
|
|
32
|
-
expect(calls[0][0]).toBe('https://specs.example.com/specs/notebook');
|
|
33
|
-
expect((calls[0][1]?.headers as Record<string, string>).Authorization).toBe(
|
|
34
|
-
'Bearer jwt'
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('tolerates a trailing slash on the service url', async () => {
|
|
39
|
-
const calls: string[] = [];
|
|
40
|
-
const fetcher = (async (url: string) => {
|
|
41
|
-
calls.push(url);
|
|
42
|
-
return response([]);
|
|
43
|
-
}) as unknown as typeof fetch;
|
|
44
|
-
|
|
45
|
-
await listNotebooks('https://specs.example.com//', 'jwt', fetcher);
|
|
46
|
-
|
|
47
|
-
expect(calls[0]).toBe('https://specs.example.com/specs/notebook');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('reports the status rather than returning an empty list', async () => {
|
|
51
|
-
// The store answers a bad or expired token with 400, so silently returning []
|
|
52
|
-
// would look exactly like an account with no notebooks.
|
|
53
|
-
const fetcher = (async () =>
|
|
54
|
-
response({ detail: 'bad' }, 400)) as unknown as typeof fetch;
|
|
55
|
-
await expect(
|
|
56
|
-
listNotebooks('https://specs.example.com', 'jwt', fetcher)
|
|
57
|
-
).rejects.toThrow('HTTP 400');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('rejects a body that is not a list', async () => {
|
|
61
|
-
const fetcher = (async () =>
|
|
62
|
-
response({ detail: 'nope' })) as unknown as typeof fetch;
|
|
63
|
-
await expect(
|
|
64
|
-
listNotebooks('https://specs.example.com', 'jwt', fetcher)
|
|
65
|
-
).rejects.toThrow('unexpected response');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('drops entries that are not summaries rather than rendering undefined', async () => {
|
|
69
|
-
const fetcher = (async () =>
|
|
70
|
-
response([
|
|
71
|
-
summary({ id: 'good' }),
|
|
72
|
-
{ id: 'bad' },
|
|
73
|
-
null
|
|
74
|
-
])) as unknown as typeof fetch;
|
|
75
|
-
const items = await listNotebooks(
|
|
76
|
-
'https://specs.example.com',
|
|
77
|
-
'jwt',
|
|
78
|
-
fetcher
|
|
79
|
-
);
|
|
80
|
-
expect(items.map(item => item.id)).toEqual(['good']);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('partitionNotebooks', () => {
|
|
85
|
-
it('splits the caller own records from the rest', () => {
|
|
86
|
-
const mine = summary({ id: 'mine', creator: 'dave@oceanum.science' });
|
|
87
|
-
const theirs = summary({ id: 'theirs', creator: null });
|
|
88
|
-
const { mine: own, shared } = partitionNotebooks(
|
|
89
|
-
[mine, theirs],
|
|
90
|
-
'dave@oceanum.science'
|
|
91
|
-
);
|
|
92
|
-
expect(own.map(i => i.id)).toEqual(['mine']);
|
|
93
|
-
expect(shared.map(i => i.id)).toEqual(['theirs']);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('compares addresses case-insensitively', () => {
|
|
97
|
-
// The store matches entities as case-sensitive globs, so the claim and the stored
|
|
98
|
-
// creator can differ in case for the same person.
|
|
99
|
-
const { mine } = partitionNotebooks(
|
|
100
|
-
[summary({ creator: 'Dave@Oceanum.Science' })],
|
|
101
|
-
'dave@oceanum.science'
|
|
102
|
-
);
|
|
103
|
-
expect(mine).toHaveLength(1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('treats everything as shared when nobody is signed in', () => {
|
|
107
|
-
const { mine, shared } = partitionNotebooks(
|
|
108
|
-
[summary({ creator: null }), summary({ id: 'b', creator: 'a@b.com' })],
|
|
109
|
-
null
|
|
110
|
-
);
|
|
111
|
-
expect(mine).toEqual([]);
|
|
112
|
-
expect(shared).toHaveLength(2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('orders each group newest first without mutating the input', () => {
|
|
116
|
-
const input = [
|
|
117
|
-
summary({ id: 'old', modified: '2026-01-01T00:00:00' }),
|
|
118
|
-
summary({ id: 'new', modified: '2026-05-17T00:00:00' })
|
|
119
|
-
];
|
|
120
|
-
expect(partitionNotebooks(input, null).shared.map(i => i.id)).toEqual([
|
|
121
|
-
'new',
|
|
122
|
-
'old'
|
|
123
|
-
]);
|
|
124
|
-
expect(input.map(i => i.id)).toEqual(['old', 'new']);
|
|
125
|
-
});
|
|
126
|
-
});
|
package/src/specStore.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
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
|
-
}
|