CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/app-framework/index.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Not sure where this should go...
7
declare global {
8
interface Window {
9
Primus: any;
10
}
11
}
12
13
// Important: code below now assumes that a global variable called "DEBUG" is **defined**!
14
declare var DEBUG: boolean;
15
if (DEBUG == null) {
16
var DEBUG = false;
17
}
18
19
let rclass: <P extends object>(
20
Component: React.ComponentType<P>,
21
) => React.ComponentType<P>;
22
23
import React from "react";
24
import createReactClass from "create-react-class";
25
import { Provider, connect, useSelector } from "react-redux";
26
import json_stable from "json-stable-stringify";
27
28
import { Store } from "@cocalc/util/redux/Store";
29
import { Actions } from "@cocalc/util/redux/Actions";
30
import { AppRedux as AppReduxBase } from "@cocalc/util/redux/AppRedux";
31
import { Table, TableConstructor } from "./Table";
32
33
// Relative import is temporary, until I figure this out -- needed for *project*
34
import { bind_methods, keys, is_valid_uuid_string } from "@cocalc/util/misc";
35
export { TypedMap, createTypedMap } from "@cocalc/util/redux/TypedMap";
36
import type { ClassMap } from "@cocalc/util/redux/types";
37
import { redux_name, project_redux_name } from "@cocalc/util/redux/name";
38
export { redux_name, project_redux_name };
39
import { NAME_TYPE as ComputeImageStoreType } from "../custom-software/util";
40
import { NEWS } from "@cocalc/frontend/notifications/news/init";
41
42
import * as types from "./actions-and-stores";
43
import type { ProjectStore } from "../project_store";
44
import type { ProjectActions } from "../project_actions";
45
export type { ProjectStore, ProjectActions };
46
47
export class AppRedux extends AppReduxBase {
48
private _tables: ClassMap<any, Table>;
49
50
constructor() {
51
super();
52
bind_methods(this);
53
this._tables = {};
54
}
55
56
getActions(name: "account"): types.AccountActions;
57
getActions(name: "projects"): types.ProjectsActions;
58
getActions(name: "billing"): types.BillingActions;
59
getActions(name: "page"): types.PageActions;
60
getActions(name: "users"): types.UsersActions;
61
getActions(name: "admin-users"): types.AdminUsersActions;
62
getActions(name: "admin-site-licenses"): types.SiteLicensesActions;
63
getActions(name: "mentions"): types.MentionsActions;
64
getActions(name: "file_use"): types.FileUseActions;
65
getActions(name: typeof NEWS): types.NewsActions;
66
getActions(name: { project_id: string }): ProjectActions;
67
getActions<T, C extends Actions<T>>(name: string): C;
68
getActions<T, C extends Actions<T>>(
69
name: string | { project_id: string },
70
): C | ProjectActions | undefined {
71
if (typeof name === "string") {
72
if (!this.hasActions(name)) {
73
return undefined;
74
} else {
75
return this._actions[name];
76
}
77
} else {
78
if (name.project_id == null) {
79
throw Error("Object must have project_id attribute");
80
}
81
return this.getProjectActions(name.project_id);
82
}
83
}
84
85
getStore(name: "account"): types.AccountStore;
86
getStore(name: "projects"): types.ProjectsStore;
87
getStore(name: "billing"): types.BillingStore;
88
getStore(name: "page"): types.PageStore;
89
getStore(name: "admin-users"): types.AdminUsersStore;
90
getStore(name: "admin-site-licenses"): types.SiteLicensesStore;
91
getStore(name: "mentions"): types.MentionsStore;
92
getStore(name: "file_use"): types.FileUseStore;
93
getStore(name: "customize"): types.CustomizeStore;
94
getStore(name: "users"): types.UsersStore;
95
getStore(name: ComputeImageStoreType): types.ComputeImagesStore;
96
getStore(name: typeof NEWS): types.NewsStore;
97
getStore<State extends Record<string, any>>(name: string): Store<State>;
98
getStore<State extends Record<string, any>, C extends Store<State>>(
99
nam: string,
100
): C | undefined;
101
getStore(name) {
102
return super.getStore(name);
103
}
104
105
getProjectsStore(): types.ProjectsStore {
106
return this.getStore("projects");
107
}
108
109
createTable<T extends Table>(
110
name: string,
111
table_class: TableConstructor<T>,
112
): T {
113
const tables = this._tables;
114
if (tables[name] != null) {
115
throw Error(`createTable: table "${name}" already exists`);
116
}
117
const table = new table_class(name, this);
118
return (tables[name] = table);
119
}
120
121
// Set the table; we assume that the table being overwritten
122
// has been cleaned up properly somehow...
123
setTable(name: string, table: Table): void {
124
this._tables[name] = table;
125
}
126
127
removeTable(name: string): void {
128
if (this._tables[name] != null) {
129
if (this._tables[name]._table != null) {
130
this._tables[name]._table.close();
131
}
132
delete this._tables[name];
133
}
134
}
135
136
getTable<T extends Table>(name: string): T {
137
if (this._tables[name] == null) {
138
throw Error(`getTable: table "${name}" not registered`);
139
}
140
return this._tables[name];
141
}
142
143
/**
144
* A React Hook to connect a function component to a project store.
145
* Opposed to `getProjectStore`, the project store will not initialize
146
* if it's not defined already.
147
*
148
* @param selectFrom selector to run on the store.
149
* The result will be compared to the previous result to determine
150
* if the component should rerender
151
* @param project_id id of the project to connect to
152
*/
153
useProjectStore<T>(
154
selectFrom: (store?: ProjectStore) => T,
155
project_id?: string,
156
): T {
157
return useSelector<any, T>((_) => {
158
let projectStore = undefined;
159
if (project_id) {
160
projectStore = this.getStore(project_redux_name(project_id)) as any;
161
}
162
return selectFrom(projectStore);
163
});
164
}
165
166
// getProject... is safe to call any time. All structures will be created
167
// if they don't exist
168
getProjectStore(project_id: string): ProjectStore {
169
if (!is_valid_uuid_string(project_id)) {
170
throw Error(`getProjectStore: INVALID project_id -- "${project_id}"`);
171
}
172
if (!this.hasProjectStore(project_id)) {
173
// Right now importing project_store breaks the share server,
174
// so we don't yet.
175
return require("../project_store").init(project_id, this);
176
} else {
177
return this.getStore(project_redux_name(project_id)) as any;
178
}
179
}
180
181
// TODO -- Typing: Type project Actions
182
// T, C extends Actions<T>
183
getProjectActions(project_id: string): ProjectActions {
184
if (!is_valid_uuid_string(project_id)) {
185
throw Error(`getProjectActions: INVALID project_id -- "${project_id}"`);
186
}
187
if (!this.hasProjectStore(project_id)) {
188
require("../project_store").init(project_id, this);
189
}
190
return this.getActions(project_redux_name(project_id)) as any;
191
}
192
// TODO -- Typing: Type project Table
193
getProjectTable(project_id: string, name: string): any {
194
if (!is_valid_uuid_string(project_id)) {
195
throw Error(`getProjectTable: INVALID project_id -- "${project_id}"`);
196
}
197
if (!this.hasProjectStore(project_id)) {
198
require("../project_store").init(project_id, this);
199
}
200
return this.getTable(project_redux_name(project_id, name));
201
}
202
203
removeProjectReferences(project_id: string): void {
204
if (!is_valid_uuid_string(project_id)) {
205
throw Error(
206
`getProjectReferences: INVALID project_id -- "${project_id}"`,
207
);
208
}
209
const name = project_redux_name(project_id);
210
const store = this.getStore(name);
211
if (store && typeof store.destroy == "function") {
212
store.destroy();
213
}
214
this.removeActions(name);
215
this.removeStore(name);
216
}
217
218
// getEditorActions but for whatever editor -- this is mainly meant to be used
219
// from the console when debugging, e.g., smc.redux.currentEditorActions()
220
public currentEditor(): {
221
actions: Actions<any> | undefined;
222
store: Store<any> | undefined;
223
} {
224
const project_id = this.getStore("page").get("active_top_tab");
225
if (!is_valid_uuid_string(project_id)) {
226
return { actions: undefined, store: undefined };
227
}
228
const store = this.getProjectStore(project_id);
229
const tab = store.get("active_project_tab");
230
if (!tab.startsWith("editor-")) {
231
return { actions: undefined, store: undefined };
232
}
233
const path = tab.slice("editor-".length);
234
return {
235
actions: this.getEditorActions(project_id, path),
236
store: this.getEditorStore(project_id, path),
237
};
238
}
239
}
240
241
const computed = (rtype) => {
242
const clone = rtype.bind({});
243
clone.is_computed = true;
244
return clone;
245
};
246
247
const rtypes = require("@cocalc/util/opts").types;
248
249
/*
250
Used by Provider to map app state to component props
251
252
rclass
253
reduxProps:
254
store_name :
255
prop : type
256
257
WARNING: If store not yet defined, then props will all be undefined for that store! There
258
is no warning/error in this case.
259
260
*/
261
const connect_component = (spec) => {
262
const map_state_to_props = function (state) {
263
const props = {};
264
if (state == null) {
265
return props;
266
}
267
for (const store_name in spec) {
268
if (store_name === "undefined") {
269
// "undefined" gets turned into this string when making a common mistake
270
console.warn("spec = ", spec);
271
throw Error(
272
"WARNING: redux spec is invalid because it contains 'undefined' as a key. " +
273
JSON.stringify(spec),
274
);
275
}
276
const info = spec[store_name];
277
const store: Store<any> | undefined = redux.getStore(store_name);
278
for (const prop in info) {
279
var val;
280
const type = info[prop];
281
282
if (type == null) {
283
throw Error(
284
`ERROR invalid redux spec: no type info set for prop '${prop}' in store '${store_name}', ` +
285
`where full spec has keys '${Object.keys(spec)}' ` +
286
`-- e.g. rtypes.bool vs. rtypes.boolean`,
287
);
288
}
289
290
if (store == undefined) {
291
val = undefined;
292
} else {
293
val = store.get(prop);
294
}
295
296
if (type.category === "IMMUTABLE") {
297
props[prop] = val;
298
} else {
299
props[prop] =
300
(val != null ? val.toJS : undefined) != null ? val.toJS() : val;
301
}
302
}
303
}
304
return props;
305
};
306
return connect(map_state_to_props);
307
};
308
309
/*
310
311
Takes an object to create a reactClass or a function which returns such an object.
312
313
Objects should be shaped like a react class save for a few exceptions:
314
x.reduxProps =
315
redux_store_name :
316
fields : value_type
317
name : type
318
319
x.actions must not be defined.
320
321
*/
322
323
// Uncomment (and also use below) for working on
324
// https://github.com/sagemathinc/cocalc/issues/4176
325
/*
326
function reduxPropsCheck(reduxProps: object) {
327
for (let store in reduxProps) {
328
const x = reduxProps[store];
329
if (x == null) continue;
330
for (let field in x) {
331
if (x[field] == rtypes.object) {
332
console.log(`WARNING: reduxProps object ${store}.${field}`);
333
}
334
}
335
}
336
}
337
*/
338
339
function compute_cache_key(data: { [key: string]: any }): string {
340
return json_stable(keys(data).sort());
341
}
342
343
rclass = function (x: any) {
344
let C;
345
if (typeof x === "function" && typeof x.reduxProps === "function") {
346
// using an ES6 class *and* reduxProps...
347
C = createReactClass({
348
render() {
349
if (this.cache0 == null) {
350
this.cache0 = {};
351
}
352
const reduxProps = x.reduxProps(this.props);
353
//reduxPropsCheck(reduxProps);
354
const key = compute_cache_key(reduxProps);
355
// console.log("ES6 rclass render", key);
356
if (this.cache0[key] == null) {
357
this.cache0[key] = connect_component(reduxProps)(x);
358
}
359
return React.createElement(
360
this.cache0[key],
361
this.props,
362
this.props.children,
363
);
364
},
365
});
366
return C;
367
} else if (typeof x === "function") {
368
// Creates a react class that wraps the eventual component.
369
// It calls the generator function with props as a parameter
370
// and caches the result based on reduxProps
371
const cached = createReactClass({
372
// This only caches per Component. No memory leak, but could be faster for multiple components with the same signature
373
render() {
374
if (this.cache == null) {
375
this.cache = {};
376
}
377
// OPTIMIZATION: Cache props before generating a new key.
378
// currently assumes making a new object is fast enough
379
const definition = x(this.props);
380
//reduxPropsCheck(definition.reduxProps);
381
const key = compute_cache_key(definition.reduxProps);
382
// console.log("function rclass render", key);
383
384
if (definition.actions != null) {
385
throw Error(
386
"You may not define a method named actions in an rclass. This is used to expose redux actions",
387
);
388
}
389
390
definition.actions = redux.getActions;
391
392
if (this.cache[key] == null) {
393
this.cache[key] = rclass(definition);
394
} // wait.. is this even the slow part?
395
396
return React.createElement(
397
this.cache[key],
398
this.props,
399
this.props.children,
400
);
401
},
402
});
403
404
return cached;
405
} else {
406
if (x.reduxProps != null) {
407
// Inject the propTypes based on the ones injected by reduxProps.
408
const propTypes = x.propTypes != null ? x.propTypes : {};
409
for (const store_name in x.reduxProps) {
410
const info = x.reduxProps[store_name];
411
for (const prop in info) {
412
const type = info[prop];
413
if (type !== rtypes.immutable) {
414
propTypes[prop] = type;
415
} else {
416
propTypes[prop] = rtypes.object;
417
}
418
}
419
}
420
x.propTypes = propTypes;
421
//reduxPropsCheck(propTypes);
422
}
423
424
if (x.actions != null && x.actions !== redux.getActions) {
425
throw Error(
426
"You may not define a method named actions in an rclass. This is used to expose redux actions",
427
);
428
}
429
430
x.actions = redux.getActions;
431
432
C = createReactClass(x);
433
if (x.reduxProps != null) {
434
// Make the ones comming from redux get automatically injected, as long
435
// as this component is in a heierarchy wrapped by <Redux>...</Redux>
436
C = connect_component(x.reduxProps)(C);
437
}
438
}
439
return C;
440
};
441
442
const redux = new AppRedux();
443
444
// Public interface
445
export function is_redux(obj) {
446
return obj instanceof AppRedux;
447
}
448
export function is_redux_actions(obj) {
449
return obj instanceof Actions;
450
}
451
452
/*
453
The non-tsx version of this:
454
<Provider store={redux.reduxStore}>
455
{children}
456
</Provider>
457
*/
458
export function Redux({ children }) {
459
return React.createElement(Provider, {
460
store: redux.reduxStore,
461
children,
462
}) as any;
463
}
464
465
export const Component = React.Component;
466
export type Rendered = React.ReactElement<any> | undefined;
467
export { rclass }; // use rclass to get access to reduxProps support
468
export { rtypes }; // has extra rtypes.immutable, needed for reduxProps to leave value as immutable
469
export { computed };
470
export { React };
471
export type CSS = React.CSSProperties;
472
export const { Fragment } = React;
473
export { redux }; // global redux singleton
474
export { Actions };
475
export { Table };
476
export { Store };
477
function UNSAFE_NONNULLABLE<T>(arg: T): NonNullable<T> {
478
return arg as any;
479
}
480
export { UNSAFE_NONNULLABLE };
481
482
// I'm explicitly disabling using typing with ReactDOM on purpose,
483
// because it's basically impossibly to use, and I'll probably get
484
// rid of all uses of ReactDOM.findDOMNode anyways.
485
//import ReactDOM from "react-dom";
486
//export { ReactDOM };
487
export const ReactDOM = require("react-dom");
488
489
declare var cc;
490
if (DEBUG) {
491
if (typeof cc !== "undefined" && cc !== null) {
492
cc.redux = redux;
493
} // for convenience in the browser (mainly for debugging)
494
}
495
496
/*
497
Given
498
spec =
499
foo :
500
bar : ...
501
stuff : ...
502
foo2 :
503
other : ...
504
505
the redux_fields function returns ['bar', 'stuff', 'other'].
506
*/
507
export function redux_fields(spec) {
508
const v: any[] = [];
509
for (let _ in spec) {
510
const val = spec[_];
511
for (const key in val) {
512
_ = val[key];
513
v.push(key);
514
}
515
}
516
return v;
517
}
518
519
// Export common React Hooks for convenience:
520
export * from "./hooks";
521
export * from "./redux-hooks";
522
523