Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/app-framework/index.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Not sure where this should go...6declare global {7interface Window {8Primus: any;9}10}1112// Important: code below now assumes that a global variable called "DEBUG" is **defined**!13declare var DEBUG: boolean;14if (DEBUG == null) {15var DEBUG = false;16}1718let rclass: <P extends object>(19Component: React.ComponentType<P>,20) => React.ComponentType<P>;2122import React from "react";23import createReactClass from "create-react-class";24import { Provider, connect, useSelector } from "react-redux";25import json_stable from "json-stable-stringify";2627import { Store } from "@cocalc/util/redux/Store";28import { Actions } from "@cocalc/util/redux/Actions";29import { AppRedux as AppReduxBase } from "@cocalc/util/redux/AppRedux";30import { Table, TableConstructor } from "./Table";3132// Relative import is temporary, until I figure this out -- needed for *project*33import { bind_methods, keys, is_valid_uuid_string } from "@cocalc/util/misc";34export { TypedMap, createTypedMap } from "@cocalc/util/redux/TypedMap";35import type { ClassMap } from "@cocalc/util/redux/types";36import { redux_name, project_redux_name } from "@cocalc/util/redux/name";37export { redux_name, project_redux_name };38import { NAME_TYPE as ComputeImageStoreType } from "../custom-software/util";39import { NEWS } from "@cocalc/frontend/notifications/news/init";4041import * as types from "./actions-and-stores";42import type { ProjectStore } from "../project_store";43import type { ProjectActions } from "../project_actions";44export type { ProjectStore, ProjectActions };4546export class AppRedux extends AppReduxBase {47private _tables: ClassMap<any, Table>;4849constructor() {50super();51bind_methods(this);52this._tables = {};53}5455getActions(name: "account"): types.AccountActions;56getActions(name: "projects"): types.ProjectsActions;57getActions(name: "billing"): types.BillingActions;58getActions(name: "page"): types.PageActions;59getActions(name: "users"): types.UsersActions;60getActions(name: "admin-users"): types.AdminUsersActions;61getActions(name: "admin-site-licenses"): types.SiteLicensesActions;62getActions(name: "mentions"): types.MentionsActions;63getActions(name: "file_use"): types.FileUseActions;64getActions(name: typeof NEWS): types.NewsActions;65getActions(name: { project_id: string }): ProjectActions;66getActions<T, C extends Actions<T>>(name: string): C;67getActions<T, C extends Actions<T>>(68name: string | { project_id: string },69): C | ProjectActions | undefined {70if (typeof name === "string") {71if (!this.hasActions(name)) {72return undefined;73} else {74return this._actions[name];75}76} else {77if (name.project_id == null) {78throw Error("Object must have project_id attribute");79}80return this.getProjectActions(name.project_id);81}82}8384getStore(name: "account"): types.AccountStore;85getStore(name: "projects"): types.ProjectsStore;86getStore(name: "billing"): types.BillingStore;87getStore(name: "page"): types.PageStore;88getStore(name: "admin-users"): types.AdminUsersStore;89getStore(name: "admin-site-licenses"): types.SiteLicensesStore;90getStore(name: "mentions"): types.MentionsStore;91getStore(name: "file_use"): types.FileUseStore;92getStore(name: "customize"): types.CustomizeStore;93getStore(name: "users"): types.UsersStore;94getStore(name: ComputeImageStoreType): types.ComputeImagesStore;95getStore(name: typeof NEWS): types.NewsStore;96getStore<State extends Record<string, any>>(name: string): Store<State>;97getStore<State extends Record<string, any>, C extends Store<State>>(98nam: string,99): C | undefined;100getStore(name) {101return super.getStore(name);102}103104getProjectsStore(): types.ProjectsStore {105return this.getStore("projects");106}107108createTable<T extends Table>(109name: string,110table_class: TableConstructor<T>,111): T {112const tables = this._tables;113if (tables[name] != null) {114throw Error(`createTable: table "${name}" already exists`);115}116const table = new table_class(name, this);117return (tables[name] = table);118}119120// Set the table; we assume that the table being overwritten121// has been cleaned up properly somehow...122setTable(name: string, table: Table): void {123this._tables[name] = table;124}125126removeTable(name: string): void {127if (this._tables[name] != null) {128if (this._tables[name]._table != null) {129this._tables[name]._table.close();130}131delete this._tables[name];132}133}134135getTable<T extends Table>(name: string): T {136if (this._tables[name] == null) {137throw Error(`getTable: table "${name}" not registered`);138}139return this._tables[name];140}141142/**143* A React Hook to connect a function component to a project store.144* Opposed to `getProjectStore`, the project store will not initialize145* if it's not defined already.146*147* @param selectFrom selector to run on the store.148* The result will be compared to the previous result to determine149* if the component should rerender150* @param project_id id of the project to connect to151*/152useProjectStore<T>(153selectFrom: (store?: ProjectStore) => T,154project_id?: string,155): T {156return useSelector<any, T>((_) => {157let projectStore = undefined;158if (project_id) {159projectStore = this.getStore(project_redux_name(project_id)) as any;160}161return selectFrom(projectStore);162});163}164165// getProject... is safe to call any time. All structures will be created166// if they don't exist167getProjectStore(project_id: string): ProjectStore {168if (!is_valid_uuid_string(project_id)) {169throw Error(`getProjectStore: INVALID project_id -- "${project_id}"`);170}171if (!this.hasProjectStore(project_id)) {172// Right now importing project_store breaks the share server,173// so we don't yet.174return require("../project_store").init(project_id, this);175} else {176return this.getStore(project_redux_name(project_id)) as any;177}178}179180// TODO -- Typing: Type project Actions181// T, C extends Actions<T>182getProjectActions(project_id: string): ProjectActions {183if (!is_valid_uuid_string(project_id)) {184throw Error(`getProjectActions: INVALID project_id -- "${project_id}"`);185}186if (!this.hasProjectStore(project_id)) {187require("../project_store").init(project_id, this);188}189return this.getActions(project_redux_name(project_id)) as any;190}191// TODO -- Typing: Type project Table192getProjectTable(project_id: string, name: string): any {193if (!is_valid_uuid_string(project_id)) {194throw Error(`getProjectTable: INVALID project_id -- "${project_id}"`);195}196if (!this.hasProjectStore(project_id)) {197require("../project_store").init(project_id, this);198}199return this.getTable(project_redux_name(project_id, name));200}201202removeProjectReferences(project_id: string): void {203if (!is_valid_uuid_string(project_id)) {204throw Error(205`getProjectReferences: INVALID project_id -- "${project_id}"`,206);207}208const name = project_redux_name(project_id);209const store = this.getStore(name);210if (store && typeof store.destroy == "function") {211store.destroy();212}213this.removeActions(name);214this.removeStore(name);215}216217// getEditorActions but for whatever editor -- this is mainly meant to be used218// from the console when debugging, e.g., smc.redux.currentEditorActions()219public currentEditor(): {220actions: Actions<any> | undefined;221store: Store<any> | undefined;222} {223const project_id = this.getStore("page").get("active_top_tab");224if (!is_valid_uuid_string(project_id)) {225return { actions: undefined, store: undefined };226}227const store = this.getProjectStore(project_id);228const tab = store.get("active_project_tab");229if (!tab.startsWith("editor-")) {230return { actions: undefined, store: undefined };231}232const path = tab.slice("editor-".length);233return {234actions: this.getEditorActions(project_id, path),235store: this.getEditorStore(project_id, path),236};237}238}239240const computed = (rtype) => {241const clone = rtype.bind({});242clone.is_computed = true;243return clone;244};245246const rtypes = require("@cocalc/util/opts").types;247248/*249Used by Provider to map app state to component props250251rclass252reduxProps:253store_name :254prop : type255256WARNING: If store not yet defined, then props will all be undefined for that store! There257is no warning/error in this case.258259*/260const connect_component = (spec) => {261const map_state_to_props = function (state) {262const props = {};263if (state == null) {264return props;265}266for (const store_name in spec) {267if (store_name === "undefined") {268// "undefined" gets turned into this string when making a common mistake269console.warn("spec = ", spec);270throw Error(271"WARNING: redux spec is invalid because it contains 'undefined' as a key. " +272JSON.stringify(spec),273);274}275const info = spec[store_name];276const store: Store<any> | undefined = redux.getStore(store_name);277for (const prop in info) {278var val;279const type = info[prop];280281if (type == null) {282throw Error(283`ERROR invalid redux spec: no type info set for prop '${prop}' in store '${store_name}', ` +284`where full spec has keys '${Object.keys(spec)}' ` +285`-- e.g. rtypes.bool vs. rtypes.boolean`,286);287}288289if (store == undefined) {290val = undefined;291} else {292val = store.get(prop);293}294295if (type.category === "IMMUTABLE") {296props[prop] = val;297} else {298props[prop] =299(val != null ? val.toJS : undefined) != null ? val.toJS() : val;300}301}302}303return props;304};305return connect(map_state_to_props);306};307308/*309310Takes an object to create a reactClass or a function which returns such an object.311312Objects should be shaped like a react class save for a few exceptions:313x.reduxProps =314redux_store_name :315fields : value_type316name : type317318x.actions must not be defined.319320*/321322// Uncomment (and also use below) for working on323// https://github.com/sagemathinc/cocalc/issues/4176324/*325function reduxPropsCheck(reduxProps: object) {326for (let store in reduxProps) {327const x = reduxProps[store];328if (x == null) continue;329for (let field in x) {330if (x[field] == rtypes.object) {331console.log(`WARNING: reduxProps object ${store}.${field}`);332}333}334}335}336*/337338function compute_cache_key(data: { [key: string]: any }): string {339return json_stable(keys(data).sort());340}341342rclass = function (x: any) {343let C;344if (typeof x === "function" && typeof x.reduxProps === "function") {345// using an ES6 class *and* reduxProps...346C = createReactClass({347render() {348if (this.cache0 == null) {349this.cache0 = {};350}351const reduxProps = x.reduxProps(this.props);352//reduxPropsCheck(reduxProps);353const key = compute_cache_key(reduxProps);354// console.log("ES6 rclass render", key);355if (this.cache0[key] == null) {356this.cache0[key] = connect_component(reduxProps)(x);357}358return React.createElement(359this.cache0[key],360this.props,361this.props.children,362);363},364});365return C;366} else if (typeof x === "function") {367// Creates a react class that wraps the eventual component.368// It calls the generator function with props as a parameter369// and caches the result based on reduxProps370const cached = createReactClass({371// This only caches per Component. No memory leak, but could be faster for multiple components with the same signature372render() {373if (this.cache == null) {374this.cache = {};375}376// OPTIMIZATION: Cache props before generating a new key.377// currently assumes making a new object is fast enough378const definition = x(this.props);379//reduxPropsCheck(definition.reduxProps);380const key = compute_cache_key(definition.reduxProps);381// console.log("function rclass render", key);382383if (definition.actions != null) {384throw Error(385"You may not define a method named actions in an rclass. This is used to expose redux actions",386);387}388389definition.actions = redux.getActions;390391if (this.cache[key] == null) {392this.cache[key] = rclass(definition);393} // wait.. is this even the slow part?394395return React.createElement(396this.cache[key],397this.props,398this.props.children,399);400},401});402403return cached;404} else {405if (x.reduxProps != null) {406// Inject the propTypes based on the ones injected by reduxProps.407const propTypes = x.propTypes != null ? x.propTypes : {};408for (const store_name in x.reduxProps) {409const info = x.reduxProps[store_name];410for (const prop in info) {411const type = info[prop];412if (type !== rtypes.immutable) {413propTypes[prop] = type;414} else {415propTypes[prop] = rtypes.object;416}417}418}419x.propTypes = propTypes;420//reduxPropsCheck(propTypes);421}422423if (x.actions != null && x.actions !== redux.getActions) {424throw Error(425"You may not define a method named actions in an rclass. This is used to expose redux actions",426);427}428429x.actions = redux.getActions;430431C = createReactClass(x);432if (x.reduxProps != null) {433// Make the ones comming from redux get automatically injected, as long434// as this component is in a heierarchy wrapped by <Redux>...</Redux>435C = connect_component(x.reduxProps)(C);436}437}438return C;439};440441const redux = new AppRedux();442443// Public interface444export function is_redux(obj) {445return obj instanceof AppRedux;446}447export function is_redux_actions(obj) {448return obj instanceof Actions;449}450451/*452The non-tsx version of this:453<Provider store={redux.reduxStore}>454{children}455</Provider>456*/457export function Redux({ children }) {458return React.createElement(Provider, {459store: redux.reduxStore,460children,461}) as any;462}463464export const Component = React.Component;465export type Rendered = React.ReactElement<any> | undefined;466export { rclass }; // use rclass to get access to reduxProps support467export { rtypes }; // has extra rtypes.immutable, needed for reduxProps to leave value as immutable468export { computed };469export { React };470export type CSS = React.CSSProperties;471export const { Fragment } = React;472export { redux }; // global redux singleton473export { Actions };474export { Table };475export { Store };476function UNSAFE_NONNULLABLE<T>(arg: T): NonNullable<T> {477return arg as any;478}479export { UNSAFE_NONNULLABLE };480481// I'm explicitly disabling using typing with ReactDOM on purpose,482// because it's basically impossibly to use, and I'll probably get483// rid of all uses of ReactDOM.findDOMNode anyways.484//import ReactDOM from "react-dom";485//export { ReactDOM };486export const ReactDOM = require("react-dom");487488declare var cc;489if (DEBUG) {490if (typeof cc !== "undefined" && cc !== null) {491cc.redux = redux;492} // for convenience in the browser (mainly for debugging)493}494495/*496Given497spec =498foo :499bar : ...500stuff : ...501foo2 :502other : ...503504the redux_fields function returns ['bar', 'stuff', 'other'].505*/506export function redux_fields(spec) {507const v: any[] = [];508for (let _ in spec) {509const val = spec[_];510for (const key in val) {511_ = val[key];512v.push(key);513}514}515return v;516}517518// Export common React Hooks for convenience:519export * from "./hooks";520export * from "./redux-hooks";521522523