From 3f3fbcf0c2ea60625f20f3d06388723645e78170 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 10 Feb 2020 12:07:32 -0500 Subject: server session now local --- src/server/DashSession/DashSessionAgent.ts | 5 +- .../Session/agents/applied_session_agent.ts | 58 ++++ src/server/DashSession/Session/agents/monitor.ts | 298 +++++++++++++++++++++ .../Session/agents/process_message_router.ts | 41 +++ .../Session/agents/promisified_ipc_manager.ts | 173 ++++++++++++ .../DashSession/Session/agents/server_worker.ts | 160 +++++++++++ src/server/DashSession/Session/utilities/repl.ts | 128 +++++++++ .../Session/utilities/session_config.ts | 129 +++++++++ .../DashSession/Session/utilities/utilities.ts | 37 +++ 9 files changed, 1028 insertions(+), 1 deletion(-) create mode 100644 src/server/DashSession/Session/agents/applied_session_agent.ts create mode 100644 src/server/DashSession/Session/agents/monitor.ts create mode 100644 src/server/DashSession/Session/agents/process_message_router.ts create mode 100644 src/server/DashSession/Session/agents/promisified_ipc_manager.ts create mode 100644 src/server/DashSession/Session/agents/server_worker.ts create mode 100644 src/server/DashSession/Session/utilities/repl.ts create mode 100644 src/server/DashSession/Session/utilities/session_config.ts create mode 100644 src/server/DashSession/Session/utilities/utilities.ts (limited to 'src/server/DashSession') diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index d61e9aac1..85bfe14de 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -8,8 +8,11 @@ import { launchServer, onWindows } from ".."; import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; import * as Archiver from "archiver"; import { resolve } from "path"; -import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; import rimraf = require("rimraf"); +import { AppliedSessionAgent, ExitHandler } from "./Session/agents/applied_session_agent"; +import { ServerWorker } from "./Session/agents/server_worker"; +import { Monitor } from "./Session/agents/monitor"; +import { MessageHandler } from "./Session/agents/promisified_ipc_manager"; /** * If we're the monitor (master) thread, we should launch the monitor logic for the session. diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts new file mode 100644 index 000000000..46c9e22ed --- /dev/null +++ b/src/server/DashSession/Session/agents/applied_session_agent.ts @@ -0,0 +1,58 @@ +import { isMaster } from "cluster"; +import { Monitor } from "./monitor"; +import { ServerWorker } from "./server_worker"; +import { Utilities } from "../utilities/utilities"; + +export type ExitHandler = (reason: Error | boolean) => void | Promise; + +export abstract class AppliedSessionAgent { + + // the following two methods allow the developer to create a custom + // session and use the built in customization options for each thread + protected abstract async initializeMonitor(monitor: Monitor): Promise; + protected abstract async initializeServerWorker(): Promise; + + private launched = false; + + public killSession = (reason: string, graceful = true, errorCode = 0) => { + const target = isMaster ? this.sessionMonitor : this.serverWorker; + target.killSession(reason, graceful, errorCode); + } + + private sessionMonitorRef: Monitor | undefined; + public get sessionMonitor(): Monitor { + if (!isMaster) { + this.serverWorker.emit("kill", { + graceful: false, + reason: "Cannot access the session monitor directly from the server worker thread.", + errorCode: 1 + }); + throw new Error(); + } + return this.sessionMonitorRef!; + } + + private serverWorkerRef: ServerWorker | undefined; + public get serverWorker(): ServerWorker { + if (isMaster) { + throw new Error("Cannot access the server worker directly from the session monitor thread"); + } + return this.serverWorkerRef!; + } + + public async launch(): Promise { + if (!this.launched) { + this.launched = true; + if (isMaster) { + this.sessionMonitorRef = Monitor.Create() + const sessionKey = await this.initializeMonitor(this.sessionMonitorRef); + this.sessionMonitorRef.finalize(sessionKey); + } else { + this.serverWorkerRef = await this.initializeServerWorker(); + } + } else { + throw new Error("Cannot launch a session thread more than once per process."); + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts new file mode 100644 index 000000000..1d4ea6fb5 --- /dev/null +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -0,0 +1,298 @@ +import { ExitHandler } from "./applied_session_agent"; +import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config"; +import Repl, { ReplAction } from "../utilities/repl"; +import { isWorker, setupMaster, on, Worker, fork } from "cluster"; +import { manage, MessageHandler } from "./promisified_ipc_manager"; +import { red, cyan, white, yellow, blue } from "colors"; +import { exec, ExecOptions } from "child_process"; +import { validate, ValidationError } from "jsonschema"; +import { Utilities } from "../utilities/utilities"; +import { readFileSync } from "fs"; +import IPCMessageReceiver from "./process_message_router"; +import { ServerWorker } from "./server_worker"; + +/** + * Validates and reads the configuration file, accordingly builds a child process factory + * and spawns off an initial process that will respawn as predecessors die. + */ +export class Monitor extends IPCMessageReceiver { + private static count = 0; + private finalized = false; + private exitHandlers: ExitHandler[] = []; + private readonly config: Configuration; + private activeWorker: Worker | undefined; + private key: string | undefined; + private repl: Repl; + + public static Create() { + if (isWorker) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create a monitor on the worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else if (++Monitor.count > 1) { + console.error(red("cannot create more than one monitor.")); + process.exit(1); + } else { + return new Monitor(); + } + } + + private constructor() { + super(); + console.log(this.timestamp(), cyan("initializing session...")); + this.configureInternalHandlers(); + this.config = this.loadAndValidateConfiguration(); + this.initializeClusterFunctions(); + this.repl = this.initializeRepl(); + } + + protected configureInternalHandlers = () => { + // handle exceptions in the master thread - there shouldn't be many of these + // the IPC (inter process communication) channel closed exception can't seem + // to be caught in a try catch, and is inconsequential, so it is ignored + process.on("uncaughtException", ({ message, stack }): void => { + if (message !== "Channel closed") { + this.mainLog(red(message)); + if (stack) { + this.mainLog(`uncaught exception\n${red(stack)}`); + } + } + }); + + this.on("kill", ({ reason, graceful, errorCode }) => this.killSession(reason, graceful, errorCode)); + this.on("lifecycle", ({ event }) => console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${event})`)); + } + + private initializeClusterFunctions = () => { + // determines whether or not we see the compilation / initialization / runtime output of each child server process + const output = this.config.showServerOutput ? "inherit" : "ignore"; + setupMaster({ stdio: ["ignore", output, output, "ipc"] }); + + // a helpful cluster event called on the master thread each time a child process exits + on("exit", ({ process: { pid } }, code, signal) => { + const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`; + this.mainLog(cyan(prompt)); + // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one + this.spawn(); + }); + } + + public finalize = (sessionKey: string): void => { + if (this.finalized) { + throw new Error("Session monitor is already finalized"); + } + this.finalized = true; + this.key = sessionKey; + this.spawn(); + } + + public readonly coreHooks = Object.freeze({ + onCrashDetected: (listener: MessageHandler<{ error: Error }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener), + onServerRunning: (listener: MessageHandler<{ isFirstTime: boolean }>) => this.on(Monitor.IntrinsicEvents.ServerRunning, listener) + }); + + /** + * Kill this session and its active child + * server process, either gracefully (may wait + * indefinitely, but at least allows active networking + * requests to complete) or immediately. + */ + public killSession = async (reason: string, graceful = true, errorCode = 0) => { + this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`)); + this.mainLog(`session exit reason: ${(red(reason))}`); + await this.executeExitHandlers(true); + await this.killActiveWorker(graceful, true); + process.exit(errorCode); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Extend the default repl by adding in custom commands + * that can invoke application logic external to this module + */ + public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + this.repl.registerCommand(basename, argPatterns, action); + } + + public exec = (command: string, options?: ExecOptions) => { + return new Promise(resolve => { + exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => { + if (error) { + this.execLog(red(`unable to execute ${white(command)}`)); + error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`))); + } else { + let outLines: string[], errorLines: string[]; + if ((outLines = stdout.split("\n").filter(line => line.length)).length) { + outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`))); + } + if ((errorLines = stderr.split("\n").filter(line => line.length)).length) { + errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`))); + } + } + resolve(); + }); + }); + } + + /** + * Generates a blue UTC string associated with the time + * of invocation. + */ + private timestamp = () => blue(`[${new Date().toUTCString()}]`); + + /** + * A formatted, identified and timestamped log in color + */ + public mainLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams); + } + + /** + * A formatted, identified and timestamped log in color for non- + */ + private execLog = (...optionalParams: any[]) => { + console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams); + } + + /** + * Reads in configuration .json file only once, in the master thread + * and pass down any variables the pertinent to the child processes as environment variables. + */ + private loadAndValidateConfiguration = (): Configuration => { + let config: Configuration; + try { + console.log(this.timestamp(), cyan("validating configuration...")); + config = JSON.parse(readFileSync('./session.config.json', 'utf8')); + const options = { + throwError: true, + allowUnknownAttributes: false + }; + // ensure all necessary and no excess information is specified by the configuration file + validate(config, configurationSchema, options); + config = Utilities.preciseAssign({}, defaultConfig, config); + } catch (error) { + if (error instanceof ValidationError) { + console.log(red("\nSession configuration failed.")); + console.log("The given session.config.json configuration file is invalid."); + console.log(`${error.instance}: ${error.stack}`); + process.exit(0); + } else if (error.code === "ENOENT" && error.path === "./session.config.json") { + console.log(cyan("Loading default session parameters...")); + console.log("Consider including a session.config.json configuration file in your project root for customization."); + config = Utilities.preciseAssign({}, defaultConfig); + } else { + console.log(red("\nSession configuration failed.")); + console.log("The following unknown error occurred during configuration."); + console.log(error.stack); + process.exit(0); + } + } finally { + const { identifiers } = config!; + Object.keys(identifiers).forEach(key => { + const resolved = key as keyof Identifiers; + const { text, color } = identifiers[resolved]; + identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`); + }); + return config!; + } + } + + /** + * Builds the repl that allows the following commands to be typed into stdin of the master thread. + */ + private initializeRepl = (): Repl => { + const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` }); + const boolean = /true|false/; + const number = /\d+/; + const letters = /[a-zA-Z]+/; + repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0)); + repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean")); + repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true")); + repl.registerCommand("set", [/polling/, number, boolean], args => { + const newPollingIntervalSeconds = Math.floor(Number(args[1])); + if (newPollingIntervalSeconds < 0) { + this.mainLog(red("the polling interval must be a non-negative integer")); + } else { + if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { + this.config.polling.intervalSeconds = newPollingIntervalSeconds; + if (args[2] === "true") { + Monitor.IPCManager.emit("updatePollingInterval", { newPollingIntervalSeconds }); + } + } + } + }); + return repl; + } + + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Attempts to kill the active worker gracefully, unless otherwise specified. + */ + private killActiveWorker = async (graceful = true, isSessionEnd = false): Promise => { + if (this.activeWorker && !this.activeWorker.isDead()) { + if (graceful) { + Monitor.IPCManager.emit("manualExit", { isSessionEnd }); + } else { + await ServerWorker.IPCManager.destroy(); + this.activeWorker.process.kill(); + } + } + } + + /** + * Allows the caller to set the port at which the target (be it the server, + * the websocket, some other custom port) is listening. If an immediate restart + * is specified, this monitor will kill the active child and re-launch the server + * at the port. Otherwise, the updated port won't be used until / unless the child + * dies on its own and triggers a restart. + */ + private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => { + if (value > 1023 && value < 65536) { + this.config.ports[port] = value; + if (immediateRestart) { + this.killActiveWorker(); + } + } else { + this.mainLog(red(`${port} is an invalid port number`)); + } + } + + /** + * Kills the current active worker and proceeds to spawn a new worker, + * feeding in configuration information as environment variables. + */ + private spawn = async (): Promise => { + await this.killActiveWorker(); + const { config: { polling, ports }, key } = this; + this.activeWorker = fork({ + pollingRoute: polling.route, + pollingFailureTolerance: polling.failureTolerance, + serverPort: ports.server, + socketPort: ports.socket, + pollingIntervalSeconds: polling.intervalSeconds, + session_key: key + }); + Monitor.IPCManager = manage(this.activeWorker.process, this.handlers); + this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`)); + } + +} + +export namespace Monitor { + + export enum IntrinsicEvents { + KeyGenerated = "key_generated", + CrashDetected = "crash_detected", + ServerRunning = "server_running" + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts new file mode 100644 index 000000000..6cc8aa941 --- /dev/null +++ b/src/server/DashSession/Session/agents/process_message_router.ts @@ -0,0 +1,41 @@ +import { MessageHandler, PromisifiedIPCManager, HandlerMap } from "./promisified_ipc_manager"; + +export default abstract class IPCMessageReceiver { + + protected static IPCManager: PromisifiedIPCManager; + protected handlers: HandlerMap = {}; + + protected abstract configureInternalHandlers: () => void; + + /** + * Add a listener at this message. When the monitor process + * receives a message, it will invoke all registered functions. + */ + public on = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (!handlers) { + this.handlers[name] = [handler]; + } else { + handlers.push(handler); + } + } + + /** + * Unregister a given listener at this message. + */ + public off = (name: string, handler: MessageHandler) => { + const handlers = this.handlers[name]; + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + /** + * Unregister all listeners at this message. + */ + public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]); + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts new file mode 100644 index 000000000..9f0db8330 --- /dev/null +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -0,0 +1,173 @@ +import { Utilities } from "../utilities/utilities"; +import { ChildProcess } from "child_process"; + +/** + * Convenience constructor + * @param target the process / worker to which to attach the specialized listeners + */ +export function manage(target: IPCTarget, handlers?: HandlerMap) { + return new PromisifiedIPCManager(target, handlers); +} + +/** + * Captures the logic to execute upon receiving a message + * of a certain name. + */ +export type HandlerMap = { [name: string]: MessageHandler[] }; + +/** + * This will always literally be a child process. But, though setting + * up a manager in the parent will indeed see the target as the ChildProcess, + * setting up a manager in the child will just see itself as a regular NodeJS.Process. + */ +export type IPCTarget = NodeJS.Process | ChildProcess; + +/** + * Specifies a general message format for this API + */ +export type Message = { + name: string; + args?: T; +}; +export type MessageHandler = (args: T) => (any | Promise); + +/** + * When a message is emitted, it is embedded with private metadata + * to facilitate the resolution of promises, etc. + */ +interface InternalMessage extends Message { metadata: Metadata } +interface Metadata { isResponse: boolean; id: string } +type InternalMessageHandler = (message: InternalMessage) => (any | Promise); + +/** + * Allows for the transmission of the error's key features over IPC. + */ +export interface ErrorLike { + name?: string; + message?: string; + stack?: string; +} + +/** + * The arguments returned in a message sent from the target upon completion. + */ +export interface Response { + results?: T[]; + error?: ErrorLike; +} + +const destroyEvent = "__destroy__"; + +/** + * This is a wrapper utility class that allows the caller process + * to emit an event and return a promise that resolves when it and all + * other processes listening to its emission of this event have completed. + */ +export class PromisifiedIPCManager { + private readonly target: IPCTarget; + private pendingMessages: { [id: string]: string } = {}; + private isDestroyed = false; + private get callerIsTarget() { + return process.pid === this.target.pid; + } + + constructor(target: IPCTarget, handlers?: HandlerMap) { + this.target = target; + if (handlers) { + handlers[destroyEvent] = [this.destroyHelper]; + this.target.addListener("message", this.generateInternalHandler(handlers)); + } + } + + /** + * This routine uniquely identifies each message, then adds a general + * message listener that waits for a response with the same id before resolving + * the promise. + */ + public emit = async (name: string, args?: any): Promise> => { + if (this.isDestroyed) { + const error = { name: "FailedDispatch", message: "Cannot use a destroyed IPC manager to emit a message." }; + return { error }; + } + return new Promise>(resolve => { + const messageId = Utilities.guid(); + const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args }) => { + if (isResponse && id === messageId) { + this.target.removeListener("message", responseHandler); + resolve(args); + } + }; + this.target.addListener("message", responseHandler); + const message = { name, args, metadata: { id: messageId, isResponse: false } }; + if (!(this.target.send && this.target.send(message))) { + const error: ErrorLike = { name: "FailedDispatch", message: "Either the target's send method was undefined or the act of sending failed." }; + resolve({ error }); + this.target.removeListener("message", responseHandler); + } + }); + } + + /** + * Invoked from either the parent or the child process, this allows + * any unresolved promises to continue in the target process, but dispatches a dummy + * completion response for each of the pending messages, allowing their + * promises in the caller to resolve. + */ + public destroy = () => { + return new Promise(async resolve => { + if (this.callerIsTarget) { + this.destroyHelper(); + } else { + await this.emit(destroyEvent); + } + resolve(); + }); + } + + /** + * Dispatches the dummy responses and sets the isDestroyed flag to true. + */ + private destroyHelper = () => { + const { pendingMessages } = this; + this.isDestroyed = true; + Object.keys(pendingMessages).forEach(id => { + const error: ErrorLike = { name: "ManagerDestroyed", message: "The IPC manager was destroyed before the response could be returned." }; + const message: InternalMessage = { name: pendingMessages[id], args: { error }, metadata: { id, isResponse: true } }; + this.target.send?.(message) + }); + this.pendingMessages = {}; + } + + /** + * This routine receives a uniquely identified message. If the message is itself a response, + * it is ignored to avoid infinite mutual responses. Otherwise, the routine awaits its completion using whatever + * router the caller has installed, and then sends a response containing the original message id, + * which will ultimately invoke the responseHandler of the original emission and resolve the + * sender's promise. + */ + private generateInternalHandler = (handlers: HandlerMap): MessageHandler => async (message: InternalMessage) => { + const { name, args, metadata } = message; + if (name && metadata && !metadata.isResponse) { + const { id } = metadata; + this.pendingMessages[id] = name; + let error: Error | undefined; + let results: any[] | undefined; + try { + const registered = handlers[name]; + if (registered) { + results = await Promise.all(registered.map(handler => handler(args))); + } + } catch (e) { + error = e; + } + if (!this.isDestroyed && this.target.send) { + const metadata = { id, isResponse: true }; + const response: Response = { results , error }; + const message = { name, args: response , metadata }; + delete this.pendingMessages[id]; + this.target.send(message); + } + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts new file mode 100644 index 000000000..976d27226 --- /dev/null +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -0,0 +1,160 @@ +import { ExitHandler } from "./applied_session_agent"; +import { isMaster } from "cluster"; +import { manage } from "./promisified_ipc_manager"; +import IPCMessageReceiver from "./process_message_router"; +import { red, green, white, yellow } from "colors"; +import { get } from "request-promise"; +import { Monitor } from "./monitor"; + +/** + * Effectively, each worker repairs the connection to the server by reintroducing a consistent state + * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification + * email if the server encounters an uncaught exception or if the server cannot be reached. + */ +export class ServerWorker extends IPCMessageReceiver { + private static count = 0; + private shouldServerBeResponsive = false; + private exitHandlers: ExitHandler[] = []; + private pollingFailureCount = 0; + private pollingIntervalSeconds: number; + private pollingFailureTolerance: number; + private pollTarget: string; + private serverPort: number; + private isInitialized = false; + + public static Create(work: Function) { + if (isMaster) { + console.error(red("cannot create a worker on the monitor process.")); + process.exit(1); + } else if (++ServerWorker.count > 1) { + ServerWorker.IPCManager.emit("kill", { + reason: "cannot create more than one worker on a given worker process.", + graceful: false, + errorCode: 1 + }); + process.exit(1); + } else { + return new ServerWorker(work); + } + } + + /** + * Allows developers to invoke application specific logic + * by hooking into the exiting of the server process. + */ + public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler); + + /** + * Kill the session monitor (parent process) from this + * server worker (child process). This will also kill + * this process (child process). + */ + public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit("kill", { reason, graceful, errorCode }); + + /** + * A convenience wrapper to tell the session monitor (parent process) + * to carry out the action with the specified message and arguments. + */ + public emit = async (name: string, args?: any) => ServerWorker.IPCManager.emit(name, args); + + private constructor(work: Function) { + super(); + this.configureInternalHandlers(); + ServerWorker.IPCManager = manage(process, this.handlers); + this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); + + const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; + this.serverPort = Number(serverPort); + this.pollingIntervalSeconds = Number(pollingIntervalSeconds); + this.pollingFailureTolerance = Number(pollingFailureTolerance); + this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`; + + work(); + this.pollServer(); + } + + /** + * Set up message and uncaught exception handlers for this + * server process. + */ + protected configureInternalHandlers = () => { + // updates the local values of variables to the those sent from master + this.on("updatePollingInterval", ({ newPollingIntervalSeconds }) => this.pollingIntervalSeconds = newPollingIntervalSeconds); + this.on("manualExit", async ({ isSessionEnd }) => { + await ServerWorker.IPCManager.destroy(); + await this.executeExitHandlers(isSessionEnd); + process.exit(0); + }); + + // one reason to exit, as the process might be in an inconsistent state after such an exception + process.on('uncaughtException', this.proactiveUnplannedExit); + process.on('unhandledRejection', reason => { + const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); + this.proactiveUnplannedExit(appropriateError); + }); + } + + /** + * Execute the list of functions registered to be called + * whenever the process exits. + */ + private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason))); + + /** + * Notify master thread (which will log update in the console) of initialization via IPC. + */ + public lifecycleNotification = (event: string) => this.emit("lifecycle", { event }); + + /** + * Called whenever the process has a reason to terminate, either through an uncaught exception + * in the process (potentially inconsistent state) or the server cannot be reached. + */ + private proactiveUnplannedExit = async (error: Error): Promise => { + this.shouldServerBeResponsive = false; + // communicates via IPC to the master thread that it should dispatch a crash notification email + this.emit(Monitor.IntrinsicEvents.CrashDetected, { error }); + await this.executeExitHandlers(error); + // notify master thread (which will log update in the console) of crash event via IPC + this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`)); + this.lifecycleNotification(red(error.message)); + await ServerWorker.IPCManager.destroy(); + process.exit(1); + } + + /** + * This monitors the health of the server by submitting a get request to whatever port / route specified + * by the configuration every n seconds, where n is also given by the configuration. + */ + private pollServer = async (): Promise => { + await new Promise(resolve => { + setTimeout(async () => { + try { + await get(this.pollTarget); + if (!this.shouldServerBeResponsive) { + // notify monitor thread that the server is up and running + this.lifecycleNotification(green(`listening on ${this.serverPort}...`)); + this.emit(Monitor.IntrinsicEvents.ServerRunning, { isFirstTime: !this.isInitialized }); + this.isInitialized = true; + } + this.shouldServerBeResponsive = true; + } catch (error) { + // if we expect the server to be unavailable, i.e. during compilation, + // the listening variable is false, activeExit will return early and the child + // process will continue + if (this.shouldServerBeResponsive) { + if (++this.pollingFailureCount > this.pollingFailureTolerance) { + this.proactiveUnplannedExit(error); + } else { + this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`)); + } + } + } finally { + resolve(); + } + }, 1000 * this.pollingIntervalSeconds); + }); + // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed + this.pollServer(); + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/repl.ts b/src/server/DashSession/Session/utilities/repl.ts new file mode 100644 index 000000000..643141286 --- /dev/null +++ b/src/server/DashSession/Session/utilities/repl.ts @@ -0,0 +1,128 @@ +import { createInterface, Interface } from "readline"; +import { red, green, white } from "colors"; + +export interface Configuration { + identifier: () => string | string; + onInvalid?: (command: string, validCommand: boolean) => string | string; + onValid?: (success?: string) => string | string; + isCaseSensitive?: boolean; +} + +export type ReplAction = (parsedArgs: Array) => any | Promise; +export interface Registration { + argPatterns: RegExp[]; + action: ReplAction; +} + +export default class Repl { + private identifier: () => string | string; + private onInvalid: ((command: string, validCommand: boolean) => string) | string; + private onValid: ((success: string) => string) | string; + private isCaseSensitive: boolean; + private commandMap = new Map(); + public interface: Interface; + private busy = false; + private keys: string | undefined; + + constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) { + this.identifier = prompt; + this.onInvalid = onInvalid || this.usage; + this.onValid = onValid || this.success; + this.isCaseSensitive = isCaseSensitive ?? true; + this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); + } + + private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier(); + + private usage = (command: string, validCommand: boolean) => { + if (validCommand) { + const formatted = white(command); + const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n')); + return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`; + } else { + const resolved = this.keys; + if (resolved) { + return resolved; + } + const members: string[] = []; + const keys = this.commandMap.keys(); + let next: IteratorResult; + while (!(next = keys.next()).done) { + members.push(next.value); + } + return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`; + } + } + + private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`; + + public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { + const existing = this.commandMap.get(basename); + const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); + const registration = { argPatterns: converted, action }; + if (existing) { + existing.push(registration); + } else { + this.commandMap.set(basename, [registration]); + } + } + + private invalid = (command: string, validCommand: boolean) => { + console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand))); + this.busy = false; + } + + private valid = (command: string) => { + console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command))); + this.busy = false; + } + + private considerInput = async (line: string) => { + if (this.busy) { + console.log(red("Busy")); + return; + } + this.busy = true; + line = line.trim(); + if (this.isCaseSensitive) { + line = line.toLowerCase(); + } + const [command, ...args] = line.split(/\s+/g); + if (!command) { + return this.invalid(command, false); + } + const registered = this.commandMap.get(command); + if (registered) { + const { length } = args; + const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); + for (const { argPatterns, action } of candidates) { + const parsed: string[] = []; + let matched = true; + if (length) { + for (let i = 0; i < length; i++) { + let matches: RegExpExecArray | null; + if ((matches = argPatterns[i].exec(args[i])) === null) { + matched = false; + break; + } + parsed.push(matches[0]); + } + } + if (!length || matched) { + const result = action(parsed); + const resolve = () => this.valid(`${command} ${parsed.join(" ")}`); + if (result instanceof Promise) { + result.then(resolve); + } else { + resolve(); + } + return; + } + } + this.invalid(command, true); + } else { + this.invalid(command, false); + } + } + +} \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts new file mode 100644 index 000000000..b0e65dde4 --- /dev/null +++ b/src/server/DashSession/Session/utilities/session_config.ts @@ -0,0 +1,129 @@ +import { Schema } from "jsonschema"; +import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors"; + +const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/; + +const identifierProperties: Schema = { + type: "object", + properties: { + text: { + type: "string", + minLength: 1 + }, + color: { + type: "string", + pattern: colorPattern + } + } +}; + +const portProperties: Schema = { + type: "number", + minimum: 1024, + maximum: 65535 +}; + +export const configurationSchema: Schema = { + id: "/configuration", + type: "object", + properties: { + showServerOutput: { type: "boolean" }, + ports: { + type: "object", + properties: { + server: portProperties, + socket: portProperties + }, + required: ["server"], + additionalProperties: true + }, + identifiers: { + type: "object", + properties: { + master: identifierProperties, + worker: identifierProperties, + exec: identifierProperties + } + }, + polling: { + type: "object", + additionalProperties: false, + properties: { + intervalSeconds: { + type: "number", + minimum: 1, + maximum: 86400 + }, + route: { + type: "string", + pattern: /\/[a-zA-Z]*/g + }, + failureTolerance: { + type: "number", + minimum: 0, + } + } + }, + } +}; + +type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; + +export const colorMapping: Map = new Map([ + ["yellow", yellow], + ["red", red], + ["cyan", cyan], + ["green", green], + ["blue", blue], + ["magenta", magenta], + ["grey", grey], + ["gray", gray], + ["white", white], + ["black", black] +]); + +interface Identifier { + text: string; + color: ColorLabel; +} + +export interface Identifiers { + master: Identifier; + worker: Identifier; + exec: Identifier; +} + +export interface Configuration { + showServerOutput: boolean; + identifiers: Identifiers; + ports: { [description: string]: number }; + polling: { + route: string; + intervalSeconds: number; + failureTolerance: number; + }; +} + +export const defaultConfig: Configuration = { + showServerOutput: false, + identifiers: { + master: { + text: "__monitor__", + color: "yellow" + }, + worker: { + text: "__server__", + color: "magenta" + }, + exec: { + text: "__exec__", + color: "green" + } + }, + ports: { server: 3000 }, + polling: { + route: "/", + intervalSeconds: 30, + failureTolerance: 0 + } +}; \ No newline at end of file diff --git a/src/server/DashSession/Session/utilities/utilities.ts b/src/server/DashSession/Session/utilities/utilities.ts new file mode 100644 index 000000000..eb8de9d7e --- /dev/null +++ b/src/server/DashSession/Session/utilities/utilities.ts @@ -0,0 +1,37 @@ +import { v4 } from "uuid"; + +export namespace Utilities { + + export function guid() { + return v4(); + } + + /** + * At any arbitrary layer of nesting within the configuration objects, any single value that + * is not specified by the configuration is given the default counterpart. If, within an object, + * one peer is given by configuration and two are not, the one is preserved while the two are given + * the default value. + * @returns the composition of all of the assigned objects, much like Object.assign(), but with more + * granularity in the overwriting of nested objects + */ + export function preciseAssign(target: any, ...sources: any[]): any { + for (const source of sources) { + preciseAssignHelper(target, source); + } + return target; + } + + export function preciseAssignHelper(target: any, source: any) { + Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { + let targetValue: any, sourceValue: any; + if (sourceValue = source[property]) { + if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { + preciseAssignHelper(targetValue, sourceValue); + } else { + target[property] = sourceValue; + } + } + }); + } + +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 13e02e89660de831860d4e70fa6c474c77d65540 Mon Sep 17 00:00:00 2001 From: server Date: Mon, 10 Feb 2020 14:35:32 -0500 Subject: ignore repl in release server --- solr-8.3.1/server/tmp/start_3204295554151338130.properties | 11 +++++++++++ solr-8.3.1/server/tmp/start_5812170489311981381.properties | 11 +++++++++++ src/server/DashSession/Session/agents/monitor.ts | 6 +++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 solr-8.3.1/server/tmp/start_3204295554151338130.properties create mode 100644 solr-8.3.1/server/tmp/start_5812170489311981381.properties (limited to 'src/server/DashSession') diff --git a/solr-8.3.1/server/tmp/start_3204295554151338130.properties b/solr-8.3.1/server/tmp/start_3204295554151338130.properties new file mode 100644 index 000000000..8ce47f861 --- /dev/null +++ b/solr-8.3.1/server/tmp/start_3204295554151338130.properties @@ -0,0 +1,11 @@ +#start.jar properties +#Wed Jan 08 17:42:54 UTC 2020 +java.version.platform=8 +java.version=1.8.0_211 +java.version.micro=0 +jetty.home=C\:\\Users\\dash\\Documents\\Dash-Web\\solr-8.3.1\\server +java.version.minor=8 +jetty.home.uri=file\:///C\:/Users/dash/Documents/Dash-Web/solr-8.3.1/server +jetty.base=C\:\\Users\\dash\\Documents\\Dash-Web\\solr-8.3.1\\server +java.version.major=1 +jetty.base.uri=file\:///C\:/Users/dash/Documents/Dash-Web/solr-8.3.1/server diff --git a/solr-8.3.1/server/tmp/start_5812170489311981381.properties b/solr-8.3.1/server/tmp/start_5812170489311981381.properties new file mode 100644 index 000000000..c4be08baf --- /dev/null +++ b/solr-8.3.1/server/tmp/start_5812170489311981381.properties @@ -0,0 +1,11 @@ +#start.jar properties +#Thu Jan 16 17:33:01 UTC 2020 +java.version.platform=8 +java.version=1.8.0_211 +java.version.micro=0 +jetty.home=C\:\\Users\\dash\\Documents\\Dash-Web\\solr-8.3.1\\server +java.version.minor=8 +jetty.home.uri=file\:///C\:/Users/dash/Documents/Dash-Web/solr-8.3.1/server +jetty.base=C\:\\Users\\dash\\Documents\\Dash-Web\\solr-8.3.1\\server +java.version.major=1 +jetty.base.uri=file\:///C\:/Users/dash/Documents/Dash-Web/solr-8.3.1/server diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts index 1d4ea6fb5..6f8d25614 100644 --- a/src/server/DashSession/Session/agents/monitor.ts +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -22,7 +22,7 @@ export class Monitor extends IPCMessageReceiver { private readonly config: Configuration; private activeWorker: Worker | undefined; private key: string | undefined; - private repl: Repl; + // private repl: Repl; public static Create() { if (isWorker) { @@ -46,7 +46,7 @@ export class Monitor extends IPCMessageReceiver { this.configureInternalHandlers(); this.config = this.loadAndValidateConfiguration(); this.initializeClusterFunctions(); - this.repl = this.initializeRepl(); + // this.repl = this.initializeRepl(); } protected configureInternalHandlers = () => { @@ -119,7 +119,7 @@ export class Monitor extends IPCMessageReceiver { * that can invoke application logic external to this module */ public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { - this.repl.registerCommand(basename, argPatterns, action); + // this.repl.registerCommand(basename, argPatterns, action); } public exec = (command: string, options?: ExecOptions) => { -- cgit v1.2.3-70-g09d2 From ddd7696c15fa26b99650dd09a2738885063f7f61 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 11 Feb 2020 16:52:28 -0500 Subject: added secure backdoor delete route --- src/server/ApiManagers/SessionManager.ts | 9 +++++++++ src/server/DashSession/DashSessionAgent.ts | 1 + 2 files changed, 10 insertions(+) (limited to 'src/server/DashSession') diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index f1629b8f0..cd8d19778 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -53,6 +53,15 @@ export default class SessionManager extends ApiManager { }) }); + register({ + method: Method.GET, + subscription: this.secureSubscriber("delete"), + secureHandler: this.authorizedAction(({ res }) => { + sessionAgent.serverWorker.emit("delete"); + res.redirect("/home"); + }) + }); + } } \ No newline at end of file diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index 85bfe14de..1ed98cdbe 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -37,6 +37,7 @@ export class DashSessionAgent extends AppliedSessionAgent { monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); monitor.on("backup", this.backup); monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); + monitor.on("delete", WebSocket.deleteFields); monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); return sessionKey; } -- cgit v1.2.3-70-g09d2 From 14a2f361c73e468547d48792478e5e7e7e78f4d6 Mon Sep 17 00:00:00 2001 From: vellichora Date: Sat, 29 Feb 2020 13:52:53 -0500 Subject: filter UI --- package-lock.json | 41 +-- src/client/views/SearchDocBox.tsx | 34 +-- src/client/views/search/CheckBox.tsx | 146 +++++----- src/client/views/search/SearchBox.tsx | 47 +++ src/server/ApiManagers/SessionManager.ts | 20 +- src/server/DashSession/DashSessionAgent.ts | 446 ++++++++++++++--------------- src/server/index.ts | 4 +- 7 files changed, 384 insertions(+), 354 deletions(-) (limited to 'src/server/DashSession') diff --git a/package-lock.json b/package-lock.json index f6b1e80da..e60950308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5600,8 +5600,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -5619,13 +5618,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5638,18 +5635,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -5752,8 +5746,7 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -5763,7 +5756,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5776,20 +5768,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5806,7 +5795,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5887,8 +5875,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -5898,7 +5885,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5974,8 +5960,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -6005,7 +5990,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6023,7 +6007,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6062,13 +6045,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } }, diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx index 5f308ff71..5a76a882f 100644 --- a/src/client/views/SearchDocBox.tsx +++ b/src/client/views/SearchDocBox.tsx @@ -39,6 +39,8 @@ export const keyPlaceholder = "Query"; @observer export class SearchDocBox extends React.Component { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchDocBox, fieldKey); } + // @observable private _display: boolean = false; @observable private _pageX: number = 0; @observable private _pageY: number = 0; @@ -72,7 +74,7 @@ export class SearchDocBox extends React.Component { console.log("didit" ); this.query = StrCast(this.props.Document.searchText); - this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query})); + this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query })); }); if (this.inputRef.current) { @@ -89,16 +91,16 @@ export class SearchDocBox extends React.Component { @action updateKey = async (newKey: string) => { this.query = newKey; - if (newKey.length >1){ - let newdocs= await this.getAllResults(this.query) + if (newKey.length > 1) { + let newdocs = await this.getAllResults(this.query); let things = newdocs.docs console.log(things); console.log(this.content); runInAction(() => { - this.content= Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }); - }); - console.log(this.content); - } + this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }); + }); + console.log(this.content); + } //this.keyRef.current && this.keyRef.current.setIsFocused(false); @@ -161,11 +163,11 @@ export class SearchDocBox extends React.Component { enter = async (e: React.KeyboardEvent) => { console.log(e.key); if (e.key === "Enter") { - let newdocs= await this.getAllResults(this.query) + let newdocs = await this.getAllResults(this.query) let things = newdocs.docs console.log(things); - this.content=Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` }); - + this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` }); + } } @@ -407,9 +409,9 @@ export class SearchDocBox extends React.Component { return (
- s + Document={this.content} + getTransform={this.props.ScreenToLocalTransform}> + s
{ opacity: 1, transition: "0.4s opacity ease", zIndex: 99, - top:0, + top: 0, }} title={"Add Metadata"} - onClick={action(() => {this.editingMetadata = !this.editingMetadata })} + onClick={action(() => { this.editingMetadata = !this.editingMetadata })} /> -
+
{ // true = checked, false = unchecked @observable private _status: boolean; - @observable private uncheckTimeline: anime.AnimeTimelineInstance; - @observable private checkTimeline: anime.AnimeTimelineInstance; + // @observable private uncheckTimeline: anime.AnimeTimelineInstance; + // @observable private checkTimeline: anime.AnimeTimelineInstance; @observable private checkRef: any; @observable private _resetReaction?: IReactionDisposer; @@ -28,87 +28,87 @@ export class CheckBox extends React.Component{ this._status = this.props.originalStatus; this.checkRef = React.createRef(); - this.checkTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); this.uncheckTimeline = anime.timeline({ - loop: false, - autoplay: false, - direction: "normal", - }); + // this.checkTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); this.uncheckTimeline = anime.timeline({ + // loop: false, + // autoplay: false, + // direction: "normal", + // }); } - componentDidMount = () => { - this.uncheckTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - opacity: 0, - }); - this.checkTimeline.add({ - targets: this.checkRef.current, - easing: "easeInOutQuad", - duration: 500, - strokeDashoffset: [anime.setDashoffset, 0], - opacity: 1 - }); + // componentDidMount = () => { + // this.uncheckTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // opacity: 0, + // }); + // this.checkTimeline.add({ + // targets: this.checkRef.current, + // easing: "easeInOutQuad", + // duration: 500, + // strokeDashoffset: [anime.setDashoffset, 0], + // opacity: 1 + // }); - if (this.props.originalStatus) { - this.checkTimeline.play(); - this.checkTimeline.restart(); - } + // if (this.props.originalStatus) { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } - this._resetReaction = reaction( - () => this.props.parent._resetBoolean, - () => { - if (this.props.parent._resetBoolean) { - runInAction(() => { - this.reset(); - this.props.parent._resetCounter++; - if (this.props.parent._resetCounter === this.props.numCount) { - this.props.parent._resetCounter = 0; - this.props.parent._resetBoolean = false; - } - }); - } - }, - ); - } + // this._resetReaction = reaction( + // () => this.props.parent._resetBoolean, + // () => { + // if (this.props.parent._resetBoolean) { + // runInAction(() => { + // this.reset(); + // this.props.parent._resetCounter++; + // if (this.props.parent._resetCounter === this.props.numCount) { + // this.props.parent._resetCounter = 0; + // this.props.parent._resetBoolean = false; + // } + // }); + // } + // }, + // ); + // } - @action.bound - reset() { - if (this.props.default) { - if (!this._status) { - this._status = true; - this.checkTimeline.play(); - this.checkTimeline.restart(); - } - } - else { - if (this._status) { - this._status = false; - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - } + // @action.bound + // reset() { + // if (this.props.default) { + // if (!this._status) { + // this._status = true; + // this.checkTimeline.play(); + // this.checkTimeline.restart(); + // } + // } + // else { + // if (this._status) { + // this._status = false; + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // } - this.props.updateStatus(this.props.default); - } + // this.props.updateStatus(this.props.default); + // } @action.bound onClick = () => { - if (this._status) { - this.uncheckTimeline.play(); - this.uncheckTimeline.restart(); - } - else { - this.checkTimeline.play(); - this.checkTimeline.restart(); + // if (this._status) { + // this.uncheckTimeline.play(); + // this.uncheckTimeline.restart(); + // } + // else { + // this.checkTimeline.play(); + // this.checkTimeline.restart(); - } - this._status = !this._status; - this.props.updateStatus(this._status); + // } + // this._status = !this._status; + // this.props.updateStatus(this._status); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 9af8829fa..02b41bc70 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -17,6 +17,7 @@ import "./FilterBox.scss"; import "./SearchBox.scss"; import { SearchItem } from './SearchItem'; import { IconBar } from './IconBar'; +import { FieldFilters } from './FieldFilters'; library.add(faTimes); @@ -338,6 +339,9 @@ export class SearchBox extends React.Component { @computed get resultHeight() { return this._numTotalResults * 70; } + @observable private _filterOpen: boolean = false; + + render() { return (
{ e.stopPropagation(); e.preventDefault(); }}> @@ -353,6 +357,49 @@ export class SearchBox extends React.Component {
+
+
+ + {/* {this.getActiveFilters()} */} +
+
+
+ +
+
+ +
+
+
+
+
+
Required words
+
+
+ +
+
+
+
+
Filter by type of node
+
+
+
+
+
+
Filter by Basic Keys
+
+
+
+
+
+ + +
+
+
{ - const to = params.to || DashSessionAgent.notificationRecipient; - const { error } = await sessionAgent.serverWorker.emit("debug", { to }); - res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`); - }) - }); + // register({ + // method: Method.GET, + // subscription: this.secureSubscriber("debug", "to?"), + // secureHandler: this.authorizedAction(async ({ req: { params }, res }) => { + // const to = params.to || DashSessionAgent.notificationRecipient; + // const { error } = await sessionAgent.serverWorker.emit("debug", { to }); + // res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`); + // }) + // }); register({ method: Method.GET, diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index c55e01243..f22d1cbbc 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -1,223 +1,223 @@ -import { Email, pathFromRoot } from "../ActionUtilities"; -import { red, yellow, green, cyan } from "colors"; -import { get } from "request-promise"; -import { Utils } from "../../Utils"; -import { WebSocket } from "../Websocket/Websocket"; -import { MessageStore } from "../Message"; -import { launchServer, onWindows } from ".."; -import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; -import * as Archiver from "archiver"; -import { resolve } from "path"; -import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; -import rimraf = require("rimraf"); - -/** - * If we're the monitor (master) thread, we should launch the monitor logic for the session. - * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus - * our job should be to run the server. - */ -export class DashSessionAgent extends AppliedSessionAgent { - - private readonly signature = "-Dash Server Session Manager"; - private readonly releaseDesktop = pathFromRoot("../../Desktop"); - - /** - * The core method invoked when the single master thread is initialized. - * Installs event hooks, repl commands and additional IPC listeners. - */ - protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise { - await this.dispatchSessionPassword(sessionKey); - monitor.addReplCommand("pull", [], () => monitor.exec("git pull")); - monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand); - monitor.addReplCommand("backup", [], this.backup); - monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); - monitor.on("backup", this.backup); - monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); - monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); - } - - /** - * The core method invoked when a server worker thread is initialized. - * Installs logic to be executed when the server worker dies. - */ - protected async initializeServerWorker(): Promise { - const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker - worker.addExitHandler(this.notifyClient); - return worker; - } - - /** - * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally. - */ - private _remoteDebugInstructions: string | undefined; - private generateDebugInstructions = (zipName: string, target: string): string => { - if (!this._remoteDebugInstructions) { - this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" }); - } - return this._remoteDebugInstructions - .replace(/__zipname__/, zipName) - .replace(/__target__/, target) - .replace(/__signature__/, this.signature); - } - - /** - * Prepares the body of the email with information regarding a crash event. - */ - private _crashInstructions: string | undefined; - private generateCrashInstructions({ name, message, stack }: Error): string { - if (!this._crashInstructions) { - this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" }); - } - return this._crashInstructions - .replace(/__name__/, name || "[no error name found]") - .replace(/__message__/, message || "[no error message found]") - .replace(/__stack__/, stack || "[no error stack found]") - .replace(/__signature__/, this.signature); - } - - /** - * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone - * to kill the server via the /kill/:key route. - */ - private dispatchSessionPassword = async (sessionKey: string): Promise => { - const { mainLog } = this.sessionMonitor; - const { notificationRecipient } = DashSessionAgent; - mainLog(green("dispatching session key...")); - const error = await Email.dispatch({ - to: notificationRecipient, - subject: "Dash Release Session Admin Authentication Key", - content: [ - `Here's the key for this session (started @ ${new Date().toUTCString()}):`, - sessionKey, - this.signature - ].join("\n\n") - }); - if (error) { - this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`)); - mainLog(red("distribution of session key experienced errors")); - } else { - mainLog(green("successfully distributed session key to recipients")); - } - } - - /** - * This sends an email with the generated crash report. - */ - private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => { - const { mainLog } = this.sessionMonitor; - const { notificationRecipient } = DashSessionAgent; - const error = await Email.dispatch({ - to: notificationRecipient, - subject: "Dash Web Server Crash", - content: this.generateCrashInstructions(crashCause) - }); - if (error) { - this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`)); - mainLog(red("distribution of crash notification experienced errors")); - } else { - mainLog(green("successfully distributed crash notification to recipients")); - } - } - - /** - * Logic for interfacing with Solr. Either starts it, - * stops it, or rebuilds its indicies. - */ - private executeSolrCommand = async (args: string[]): Promise => { - const { exec, mainLog } = this.sessionMonitor; - const action = args[0]; - if (action === "index") { - exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") }); - } else { - const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`; - await exec(command, { cwd: "./solr-8.3.1/bin" }); - try { - await get("http://localhost:8983"); - mainLog(green("successfully connected to 8983 after running solr initialization")); - } catch { - mainLog(red("unable to connect at 8983 after running solr initialization")); - } - } - } - - /** - * Broadcast to all clients that their connection - * is no longer valid, and explain why / what to expect. - */ - private notifyClient: ExitHandler = reason => { - const { _socket } = WebSocket; - if (_socket) { - const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash"; - Utils.Emit(_socket, MessageStore.ConnectionTerminated, message); - } - } - - /** - * Performs a backup of the database, saved to the desktop subdirectory. - * This should work as is only on our specific release server. - */ - private backup = async (): Promise => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop }); - - /** - * Compress either a brand new backup or the most recent backup and send it - * as an attachment to an email, dispatched to the requested recipient. - * @param mode specifies whether or not to make a new backup before exporting - * @param to the recipient of the email - */ - private async dispatchZippedDebugBackup(to: string): Promise { - const { mainLog } = this.sessionMonitor; - try { - // if desired, complete an immediate backup to send - await this.backup(); - mainLog("backup complete"); - - const backupsDirectory = `${this.releaseDesktop}/backups`; - - // sort all backups by their modified time, and choose the most recent one - const target = readdirSync(backupsDirectory).map(filename => ({ - modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs, - filename - })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename; - mainLog(`targeting ${target}...`); - - // create a zip file and to it, write the contents of the backup directory - const zipName = `${target}.zip`; - const zipPath = `${this.releaseDesktop}/${zipName}`; - const targetPath = `${backupsDirectory}/${target}`; - const output = createWriteStream(zipPath); - const zip = Archiver('zip'); - zip.pipe(output); - zip.directory(`${targetPath}/Dash`, false); - await zip.finalize(); - mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`); - - // dispatch the email to the recipient, containing the finalized zip file - const error = await Email.dispatch({ - to, - subject: `Remote debug: compressed backup of ${target}...`, - content: this.generateDebugInstructions(zipName, target), - attachments: [{ filename: zipName, path: zipPath }] - }); - - // since this is intended to be a zero-footprint operation, clean up - // by unlinking both the backup generated earlier in the function and the compressed zip file. - // to generate a persistent backup, just run backup. - unlinkSync(zipPath); - rimraf.sync(targetPath); - - // indicate success or failure - mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`); - error && mainLog(red(error.message)); - } catch (error) { - mainLog(red("unable to dispatch zipped backup...")); - mainLog(red(error.message)); - } - } - -} - -export namespace DashSessionAgent { - - export const notificationRecipient = "brownptcdash@gmail.com"; - -} \ No newline at end of file +// import { Email, pathFromRoot } from "../ActionUtilities"; +// import { red, yellow, green, cyan } from "colors"; +// import { get } from "request-promise"; +// import { Utils } from "../../Utils"; +// import { WebSocket } from "../Websocket/Websocket"; +// import { MessageStore } from "../Message"; +// import { launchServer, onWindows } from ".."; +// import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; +// import * as Archiver from "archiver"; +// import { resolve } from "path"; +// import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session"; +// import rimraf = require("rimraf"); + +// /** +// * If we're the monitor (master) thread, we should launch the monitor logic for the session. +// * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus +// * our job should be to run the server. +// */ +// export class DashSessionAgent extends AppliedSessionAgent { + +// private readonly signature = "-Dash Server Session Manager"; +// private readonly releaseDesktop = pathFromRoot("../../Desktop"); + +// /** +// * The core method invoked when the single master thread is initialized. +// * Installs event hooks, repl commands and additional IPC listeners. +// */ +// protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise { +// await this.dispatchSessionPassword(sessionKey); +// monitor.addReplCommand("pull", [], () => monitor.exec("git pull")); +// monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand); +// monitor.addReplCommand("backup", [], this.backup); +// monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); +// monitor.on("backup", this.backup); +// monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); +// monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); +// } + +// /** +// * The core method invoked when a server worker thread is initialized. +// * Installs logic to be executed when the server worker dies. +// */ +// protected async initializeServerWorker(): Promise { +// const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker +// worker.addExitHandler(this.notifyClient); +// return worker; +// } + +// /** +// * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally. +// */ +// private _remoteDebugInstructions: string | undefined; +// private generateDebugInstructions = (zipName: string, target: string): string => { +// if (!this._remoteDebugInstructions) { +// this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" }); +// } +// return this._remoteDebugInstructions +// .replace(/__zipname__/, zipName) +// .replace(/__target__/, target) +// .replace(/__signature__/, this.signature); +// } + +// /** +// * Prepares the body of the email with information regarding a crash event. +// */ +// private _crashInstructions: string | undefined; +// private generateCrashInstructions({ name, message, stack }: Error): string { +// if (!this._crashInstructions) { +// this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" }); +// } +// return this._crashInstructions +// .replace(/__name__/, name || "[no error name found]") +// .replace(/__message__/, message || "[no error message found]") +// .replace(/__stack__/, stack || "[no error stack found]") +// .replace(/__signature__/, this.signature); +// } + +// /** +// * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone +// * to kill the server via the /kill/:key route. +// */ +// private dispatchSessionPassword = async (sessionKey: string): Promise => { +// const { mainLog } = this.sessionMonitor; +// const { notificationRecipient } = DashSessionAgent; +// mainLog(green("dispatching session key...")); +// const error = await Email.dispatch({ +// to: notificationRecipient, +// subject: "Dash Release Session Admin Authentication Key", +// content: [ +// `Here's the key for this session (started @ ${new Date().toUTCString()}):`, +// sessionKey, +// this.signature +// ].join("\n\n") +// }); +// if (error) { +// this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`)); +// mainLog(red("distribution of session key experienced errors")); +// } else { +// mainLog(green("successfully distributed session key to recipients")); +// } +// } + +// /** +// * This sends an email with the generated crash report. +// */ +// private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => { +// const { mainLog } = this.sessionMonitor; +// const { notificationRecipient } = DashSessionAgent; +// const error = await Email.dispatch({ +// to: notificationRecipient, +// subject: "Dash Web Server Crash", +// content: this.generateCrashInstructions(crashCause) +// }); +// if (error) { +// this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`)); +// mainLog(red("distribution of crash notification experienced errors")); +// } else { +// mainLog(green("successfully distributed crash notification to recipients")); +// } +// } + +// /** +// * Logic for interfacing with Solr. Either starts it, +// * stops it, or rebuilds its indicies. +// */ +// private executeSolrCommand = async (args: string[]): Promise => { +// const { exec, mainLog } = this.sessionMonitor; +// const action = args[0]; +// if (action === "index") { +// exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") }); +// } else { +// const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`; +// await exec(command, { cwd: "./solr-8.3.1/bin" }); +// try { +// await get("http://localhost:8983"); +// mainLog(green("successfully connected to 8983 after running solr initialization")); +// } catch { +// mainLog(red("unable to connect at 8983 after running solr initialization")); +// } +// } +// } + +// /** +// * Broadcast to all clients that their connection +// * is no longer valid, and explain why / what to expect. +// */ +// private notifyClient: ExitHandler = reason => { +// const { _socket } = WebSocket; +// if (_socket) { +// const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash"; +// Utils.Emit(_socket, MessageStore.ConnectionTerminated, message); +// } +// } + +// /** +// * Performs a backup of the database, saved to the desktop subdirectory. +// * This should work as is only on our specific release server. +// */ +// private backup = async (): Promise => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop }); + +// /** +// * Compress either a brand new backup or the most recent backup and send it +// * as an attachment to an email, dispatched to the requested recipient. +// * @param mode specifies whether or not to make a new backup before exporting +// * @param to the recipient of the email +// */ +// private async dispatchZippedDebugBackup(to: string): Promise { +// const { mainLog } = this.sessionMonitor; +// try { +// // if desired, complete an immediate backup to send +// await this.backup(); +// mainLog("backup complete"); + +// const backupsDirectory = `${this.releaseDesktop}/backups`; + +// // sort all backups by their modified time, and choose the most recent one +// const target = readdirSync(backupsDirectory).map(filename => ({ +// modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs, +// filename +// })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename; +// mainLog(`targeting ${target}...`); + +// // create a zip file and to it, write the contents of the backup directory +// const zipName = `${target}.zip`; +// const zipPath = `${this.releaseDesktop}/${zipName}`; +// const targetPath = `${backupsDirectory}/${target}`; +// const output = createWriteStream(zipPath); +// const zip = Archiver('zip'); +// zip.pipe(output); +// zip.directory(`${targetPath}/Dash`, false); +// await zip.finalize(); +// mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`); + +// // dispatch the email to the recipient, containing the finalized zip file +// const error = await Email.dispatch({ +// to, +// subject: `Remote debug: compressed backup of ${target}...`, +// content: this.generateDebugInstructions(zipName, target), +// attachments: [{ filename: zipName, path: zipPath }] +// }); + +// // since this is intended to be a zero-footprint operation, clean up +// // by unlinking both the backup generated earlier in the function and the compressed zip file. +// // to generate a persistent backup, just run backup. +// unlinkSync(zipPath); +// rimraf.sync(targetPath); + +// // indicate success or failure +// mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`); +// error && mainLog(red(error.message)); +// } catch (error) { +// mainLog(red("unable to dispatch zipped backup...")); +// mainLog(red(error.message)); +// } +// } + +// } + +// export namespace DashSessionAgent { + +// export const notificationRecipient = "brownptcdash@gmail.com"; + +// } \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 313a2f0e2..454d99305 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -22,7 +22,7 @@ import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; import { Logger } from "./ProcessFactory"; import { yellow } from "colors"; -import { DashSessionAgent } from "./DashSession/DashSessionAgent"; +// import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import SessionManager from "./ApiManagers/SessionManager"; import { AppliedSessionAgent } from "resilient-server-session"; @@ -141,7 +141,7 @@ export async function launchServer() { * So, the 'else' clause is exactly what we've always run when executing npm start. */ if (process.env.RELEASE) { - (sessionAgent = new DashSessionAgent()).launch(); + // (sessionAgent = new DashSessionAgent()).launch(); } else { launchServer(); } -- cgit v1.2.3-70-g09d2 From 4859d4e3f5827c3341d43d19a5309d2ec25ab67b Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Apr 2020 17:15:14 -0400 Subject: fixed warnings and some compile errors. Made a key value layout and extened DocumentBox to have a childLayoutKey field --- src/client/ClientRecommender.tsx | 54 +++++----- src/client/cognitive_services/CognitiveServices.ts | 36 +++---- src/client/documents/Documents.ts | 3 +- src/client/util/RichTextSchema.tsx | 18 ++-- src/client/views/DocumentButtonBar.tsx | 2 +- src/client/views/EditableView.tsx | 22 ++-- src/client/views/GestureOverlay.tsx | 8 +- src/client/views/GlobalKeyHandler.ts | 4 +- src/client/views/MainView.tsx | 2 + src/client/views/RecommendationsBox.tsx | 16 +-- src/client/views/TemplateMenu.tsx | 8 +- src/client/views/TouchScrollableMenu.tsx | 4 +- .../views/collections/CollectionDockingView.tsx | 2 +- .../views/collections/CollectionSchemaCells.tsx | 2 + .../views/collections/CollectionSchemaView.tsx | 8 +- .../views/collections/CollectionStackingView.tsx | 2 +- .../CollectionStackingViewFieldColumn.tsx | 3 +- .../views/collections/CollectionStaffView.tsx | 12 +-- src/client/views/collections/CollectionSubView.tsx | 3 +- .../views/collections/CollectionTimeView.tsx | 2 +- src/client/views/collections/CollectionView.tsx | 2 +- .../views/collections/CollectionViewChromes.tsx | 10 +- .../CollectionFreeFormLinksView.tsx | 116 ++++++++++----------- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +- src/client/views/nodes/ButtonBox.tsx | 2 +- .../views/nodes/ContentFittingDocumentView.tsx | 2 + src/client/views/nodes/DocumentBox.tsx | 3 +- src/client/views/nodes/DocumentContentsView.tsx | 24 +++-- src/client/views/nodes/KeyValuePair.tsx | 2 + src/client/views/nodes/RadialMenuItem.tsx | 30 +++--- src/client/views/nodes/ScreenshotBox.tsx | 2 +- src/client/views/nodes/WebBox.tsx | 2 +- src/client/views/pdf/PDFViewer.tsx | 2 +- src/client/views/webcam/DashWebRTCVideo.tsx | 8 +- src/mobile/MobileInterface.tsx | 2 + src/new_fields/Doc.ts | 4 +- src/new_fields/util.ts | 2 +- .../Session/agents/applied_session_agent.ts | 2 +- src/server/DashSession/Session/agents/monitor.ts | 2 +- .../Session/agents/promisified_ipc_manager.ts | 6 +- src/server/Websocket/Websocket.ts | 8 +- src/server/database.ts | 2 +- src/server/server_Initialization.ts | 2 +- 43 files changed, 232 insertions(+), 218 deletions(-) (limited to 'src/server/DashSession') diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx index 0e67a6e57..537e331ab 100644 --- a/src/client/ClientRecommender.tsx +++ b/src/client/ClientRecommender.tsx @@ -69,7 +69,7 @@ export class ClientRecommender extends React.Component { */ private distance(vector1: number[], vector2: number[], metric: string = "cosine") { - assert(vector1.length === vector2.length, "Vectors are not the same length"); + // assert(vector1.length === vector2.length, "Vectors are not the same length"); let similarity: number; switch (metric) { case "cosine": @@ -113,7 +113,7 @@ export class ClientRecommender extends React.Component { } } ); - let doclist = Array.from(ClientRecommender.Instance.docVectors); + const doclist = Array.from(ClientRecommender.Instance.docVectors); if (distance_metric === "euclidian") { doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => a.score - b.score); } @@ -169,12 +169,12 @@ export class ClientRecommender extends React.Component { */ generateMetadata = async (dataDoc: Doc, extDoc: Doc, threshold: Confidence = Confidence.Excellent) => { - let converter = (results: any) => { - let tagDoc = new Doc; - let tagsList = new List(); + const converter = (results: any) => { + const tagDoc = new Doc; + const tagsList = new List(); results.tags.map((tag: Tag) => { tagsList.push(tag.name); - let sanitized = tag.name.replace(" ", "_"); + const sanitized = tag.name.replace(" ", "_"); tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); extDoc.generatedTags = tagsList; @@ -193,7 +193,7 @@ export class ClientRecommender extends React.Component { */ private url(dataDoc: Doc) { - let data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField); + const data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField); return data ? data.url.href : undefined; } @@ -215,14 +215,14 @@ export class ClientRecommender extends React.Component { } } else { - let fielddata = Cast(dataDoc.data, RichTextField); - fielddata ? data = fielddata[ToPlainText]() : data = ""; + const fielddata = Cast(dataDoc.data, RichTextField, null); + data = fielddata?.Text || ""; } // STEP 2. Upon receiving response from Text Cognitive Services, do additional processing on keywords. // Currently we are still using Cognitive Services for internal recommendations, but in the future this might not be necessary. - let converter = async (results: any, data: string, isImage: boolean = false) => { + const converter = async (results: any, data: string, isImage: boolean = false) => { let keyterms = new List(); // raw keywords let kp_string: string = ""; // keywords*frequency concatenated into a string. input into TF let highKP: string[] = [""]; // most frequent keyphrase @@ -237,7 +237,7 @@ export class ClientRecommender extends React.Component { } else { // text processing results.documents.forEach((doc: any) => { - let keyPhrases = doc.keyPhrases; // returned by Cognitive Services + const keyPhrases = doc.keyPhrases; // returned by Cognitive Services keyPhrases.map((kp: string) => { keyterms.push(kp); const frequency = this.countFrequencies(kp, data); // frequency of keyphrase in paragraph @@ -308,10 +308,10 @@ export class ClientRecommender extends React.Component { */ private countFrequencies(keyphrase: string, paragraph: string) { - let data = paragraph.split(/ |\n/); // splits by new lines and spaces - let kp_array = keyphrase.split(" "); - let num_keywords = kp_array.length; - let par_length = data.length; + const data = paragraph.split(/ |\n/); // splits by new lines and spaces + const kp_array = keyphrase.split(" "); + const num_keywords = kp_array.length; + const par_length = data.length; let frequency = 0; // slides keyphrase windows across paragraph and checks if it matches with corresponding paragraph slice for (let i = 0; i <= par_length - num_keywords; i++) { @@ -353,8 +353,8 @@ export class ClientRecommender extends React.Component { bingWebSearch = async (query: string) => { const converter = async (results: any) => { - let title_vals: string[] = []; - let url_vals: string[] = []; + const title_vals: string[] = []; + const url_vals: string[] = []; results.webPages.value.forEach((doc: any) => { title_vals.push(doc.name); url_vals.push(doc.url); @@ -369,23 +369,23 @@ export class ClientRecommender extends React.Component { */ arxivrequest = async (query: string) => { - let xhttp = new XMLHttpRequest(); - let serveraddress = "http://export.arxiv.org/api"; + const xhttp = new XMLHttpRequest(); + const serveraddress = "http://export.arxiv.org/api"; const maxresults = 5; - let endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString(); - let promisified = (resolve: any, reject: any) => { + const endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString(); + const promisified = (resolve: any, reject: any) => { xhttp.onreadystatechange = function () { if (this.readyState === 4) { - let result = xhttp.response; - let xml = xhttp.responseXML; + const result = xhttp.response; + const xml = xhttp.responseXML; console.log("arXiv Result: ", xml); switch (this.status) { case 200: - let title_vals: string[] = []; - let url_vals: string[] = []; + const title_vals: string[] = []; + const url_vals: string[] = []; //console.log(result); if (xml) { - let titles = xml.getElementsByTagName("title"); + const titles = xml.getElementsByTagName("title"); let counter = 1; if (titles && titles.length > 1) { while (counter <= maxresults) { @@ -394,7 +394,7 @@ export class ClientRecommender extends React.Component { counter++; } } - let ids = xml.getElementsByTagName("id"); + const ids = xml.getElementsByTagName("id"); counter = 1; if (ids && ids.length > 1) { while (counter <= maxresults) { diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 542ccf04d..3f3726621 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -208,7 +208,7 @@ export namespace CognitiveServices { results.recognitionUnits && (results = results.recognitionUnits); } return results; - } + }; } export interface AzureStrokeData { @@ -232,13 +232,13 @@ export namespace CognitiveServices { return data; }, requester: async (apiKey: string, query: string) => { - let xhttp = new XMLHttpRequest(); - let serverAddress = "https://api.cognitive.microsoft.com"; - let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query); - let promisified = (resolve: any, reject: any) => { + const xhttp = new XMLHttpRequest(); + const serverAddress = "https://api.cognitive.microsoft.com"; + const endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query); + const promisified = (resolve: any, reject: any) => { xhttp.onreadystatechange = function () { if (this.readyState === 4) { - let result = xhttp.responseText; + const result = xhttp.responseText; switch (this.status) { case 200: return resolve(result); @@ -266,7 +266,7 @@ export namespace CognitiveServices { export namespace Appliers { export const analyzer = async (query: string, converter: BingConverter) => { - let results = await ExecuteQuery(Service.Bing, Manager, query); + const results = await ExecuteQuery(Service.Bing, Manager, query); console.log("Bing results: ", results); const { title_vals, url_vals } = await converter(results); return { title_vals, url_vals }; @@ -281,13 +281,13 @@ export namespace CognitiveServices { return data; }, requester: async (apiKey: string, query: string) => { - let xhttp = new XMLHttpRequest(); - let serverAddress = "https://babel.hathitrust.org/cgi/htd/​"; - let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query); - let promisified = (resolve: any, reject: any) => { + const xhttp = new XMLHttpRequest(); + const serverAddress = "https://babel.hathitrust.org/cgi/htd/​"; + const endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query); + const promisified = (resolve: any, reject: any) => { xhttp.onreadystatechange = function () { if (this.readyState === 4) { - let result = xhttp.responseText; + const result = xhttp.responseText; switch (this.status) { case 200: return resolve(result); @@ -315,7 +315,7 @@ export namespace CognitiveServices { export namespace Appliers { export const analyzer = async (query: string, converter: BingConverter) => { - let results = await ExecuteQuery(Service.Bing, Manager, query); + const results = await ExecuteQuery(Service.Bing, Manager, query); console.log("Bing results: ", results); const { title_vals, url_vals } = await converter(results); return { title_vals, url_vals }; @@ -337,9 +337,9 @@ export namespace CognitiveServices { }); }, requester: async (apiKey: string, body: string, service: Service) => { - let serverAddress = "https://eastus.api.cognitive.microsoft.com"; - let endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases"; - let sampleBody = { + const serverAddress = "https://eastus.api.cognitive.microsoft.com"; + const endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases"; + const sampleBody = { "documents": [ { "language": "en", @@ -348,7 +348,7 @@ export namespace CognitiveServices { } ] }; - let actualBody = body; + const actualBody = body; const options = { uri: endpoint, body: actualBody, @@ -368,7 +368,7 @@ export namespace CognitiveServices { console.log("vectorizing..."); //keyterms = ["father", "king"]; - let args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true }; + const args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true }; await requestPromise.post(args).then(async (wordvecs) => { if (wordvecs) { const indices = Object.keys(wordvecs); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index dbea8062e..96425ba30 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -266,7 +266,7 @@ export namespace Docs { options: { _width: 40, _height: 40, borderRounding: "100%" }, }], [DocumentType.RECOMMENDATION, { - layout: { view: RecommendationsBox }, + layout: { view: RecommendationsBox, dataField: data }, options: { width: 200, height: 200 }, }], [DocumentType.WEBCAM, { @@ -365,6 +365,7 @@ export namespace Docs { const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) }; options.layout = layout.view.LayoutString(layout.dataField); const doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options }); + doc.layout_keyValue = KeyValueBox.LayoutString(""); return doc; } diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index a2fb7c11b..81ab95ff5 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -810,7 +810,7 @@ export class DashDocView { this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); - let doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { + const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { ReactDOM.unmountComponentAtNode(this._dashSpan); ReactDOM.render( { if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { @@ -939,7 +939,7 @@ export class DashFieldView { this._fieldCheck.style.backgroundColor = "rgba(155, 155, 155, 0.24)"; this._fieldCheck.onchange = function (e: any) { self._dashDoc![self._fieldKey] = e.target.checked; - } + }; this._fieldSpan = document.createElement("div"); this._fieldSpan.id = Utils.GenerateGuid(); @@ -1095,12 +1095,12 @@ export class FootnoteView { "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch), "Mod-b": toggleMark(schema.marks.strong) }), - new Plugin({ - view(newView) { - // TODO -- make this work with RichTextMenu - // return FormattedTextBox.getToolTip(newView); - } - }) + // new Plugin({ + // view(newView) { + // // TODO -- make this work with RichTextMenu + // // return FormattedTextBox.getToolTip(newView); + // } + // }) ], }), diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index e673189f9..c6fb9ad0b 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -39,7 +39,7 @@ library.add(faCheckCircle); library.add(faCloudUploadAlt); library.add(faSyncAlt); library.add(faShare); -library.add(faPhotoVideo) +library.add(faPhotoVideo); const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 4a27425e8..2219966e5 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -4,10 +4,7 @@ import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../new_fields/ObjectField'; import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField'; -import { ContextMenu } from './ContextMenu'; -import { ContextMenuProps } from './ContextMenuItem'; import "./EditableView.scss"; -import { CollectionTreeView } from './collections/CollectionTreeView'; export interface EditableProps { /** @@ -88,12 +85,12 @@ export class EditableView extends React.Component { onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Tab") { e.stopPropagation(); - this.finalizeEdit(e.currentTarget.value, e.shiftKey); + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); this.props.OnTab && this.props.OnTab(e.shiftKey); } else if (e.key === "Enter") { e.stopPropagation(); if (!e.ctrlKey) { - this.finalizeEdit(e.currentTarget.value, e.shiftKey); + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); } else if (this.props.OnFillDown) { this.props.OnFillDown(e.currentTarget.value); this._editing = false; @@ -123,10 +120,17 @@ export class EditableView extends React.Component { } @action - private finalizeEdit(value: string, shiftDown: boolean) { - this._editing = false; + private finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean) { if (this.props.SetValue(value, shiftDown)) { + this._editing = false; + this.props.isEditingCallback?.(false); + } else { + this._editing = false; this.props.isEditingCallback?.(false); + !lostFocus && setTimeout(action(() => { + this._editing = true; + this.props.isEditingCallback?.(true); + }), 0); } } @@ -151,7 +155,7 @@ export class EditableView extends React.Component { className: "editableView-input", onKeyDown: this.onKeyDown, autoFocus: true, - onBlur: e => this.finalizeEdit(e.currentTarget.value, false), + onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true), onPointerDown: this.stopPropagation, onClick: this.stopPropagation, onPointerUp: this.stopPropagation, @@ -163,7 +167,7 @@ export class EditableView extends React.Component { defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus={true} - onBlur={e => this.finalizeEdit(e.currentTarget.value, false)} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize }} />; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 1eff58948..ea60907f6 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -165,7 +165,7 @@ export default class GestureOverlay extends Touchable { this._holdTimer = setTimeout(() => { console.log("hold"); const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); - let pt: any = te.touches[te.touches.length - 1]; + const pt: any = te.touches[te.touches.length - 1]; if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) { target?.dispatchEvent( new CustomEvent>("dashOnTouchHoldStart", @@ -589,7 +589,7 @@ export default class GestureOverlay extends Touchable { for (const wR of wordResults) { console.log(wR); if (wR?.recognizedText) { - possibilities.push(wR?.recognizedText) + possibilities.push(wR?.recognizedText); } possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString)); } @@ -743,16 +743,16 @@ export default class GestureOverlay extends Touchable { {this.elements}
{this._clipboardDoc}
{ - let stopPropagation = false; - let preventDefault = false; + const stopPropagation = false; + const preventDefault = false; switch (keyname) { // case "~": diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index a81e0cc2c..8d9be5980 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -521,7 +521,9 @@ export class MainView extends React.Component { DataDoc={undefined} LibraryPath={emptyPath} fieldKey={"data"} + dropAction={"alias"} annotationsKey={""} + bringToFront={emptyFunction} select={emptyFunction} active={returnFalse} isSelected={returnFalse} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx index 262226bac..5ebba0abb 100644 --- a/src/client/views/RecommendationsBox.tsx +++ b/src/client/views/RecommendationsBox.tsx @@ -6,7 +6,7 @@ import "./RecommendationsBox.scss"; import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc"; import { DocumentIcon } from "./nodes/DocumentIcon"; import { StrCast, NumCast } from "../../new_fields/Types"; -import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath } from "../../Utils"; import { Transform } from "../util/Transform"; import { ObjectField } from "../../new_fields/ObjectField"; import { DocumentView } from "./nodes/DocumentView"; @@ -31,7 +31,7 @@ library.add(faBullseye, faLink); @observer export class RecommendationsBox extends React.Component { - public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } // @observable private _display: boolean = false; @observable private _pageX: number = 0; @@ -48,17 +48,17 @@ export class RecommendationsBox extends React.Component { @action private DocumentIcon(doc: Doc) { - let layoutresult = StrCast(doc.type); + const layoutresult = StrCast(doc.type); let renderDoc = doc; //let box: number[] = []; if (layoutresult.indexOf(DocumentType.COL) !== -1) { renderDoc = Doc.MakeDelegate(renderDoc); } - let returnXDimension = () => 150; - let returnYDimension = () => 150; - let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + const returnXDimension = () => 150; + const returnYDimension = () => 150; + const scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); //let scale = () => 1; - let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt + const newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); newRenderDoc.autoHeight = false; const docview =
@@ -66,8 +66,8 @@ export class RecommendationsBox extends React.Component { fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} Document={newRenderDoc} addDocument={returnFalse} + LibraryPath={emptyPath} removeDocument={returnFalse} - ruleProvider={undefined} ScreenToLocalTransform={Transform.Identity} addDocTab={returnFalse} pinToPres={returnFalse} diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 83dbd3db3..996928cca 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -114,7 +114,7 @@ export class TemplateMenu extends React.Component { const templateName = StrCast(firstDoc.layoutKey, "layout").replace("layout_", ""); const noteTypesDoc = Cast(Doc.UserDoc().noteTypes, Doc, null); const noteTypes = DocListCast(noteTypesDoc?.data); - const addedTypes = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data) + const addedTypes = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array = []; this.props.templates.forEach((checked, template) => @@ -170,8 +170,8 @@ Scripting.addGlobal(function switchView(doc: Doc, template: Doc) { if (template.dragFactory) { template = Cast(template.dragFactory, Doc, null); } - let templateTitle = StrCast(template?.title); - return templateTitle && DocumentView.makeCustomViewClicked(doc, undefined, Docs.Create.FreeformDocument, templateTitle, template) + const templateTitle = StrCast(template?.title); + return templateTitle && DocumentView.makeCustomViewClicked(doc, undefined, Docs.Create.FreeformDocument, templateTitle, template); }); Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, firstDocTitlte: string) { @@ -180,4 +180,4 @@ Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, firstDocTitlte: st const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked'; // return SelectionManager.SelectedDocuments().some(view => StrCast(view.props.Document.layoutKey) === "layout_" + template) ? 'check' : 'unchecked' -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx index 4bda0818e..969605be9 100644 --- a/src/client/views/TouchScrollableMenu.tsx +++ b/src/client/views/TouchScrollableMenu.tsx @@ -44,7 +44,7 @@ export default class TouchScrollableMenu extends React.Component
- ) + ); } } @@ -54,6 +54,6 @@ export class TouchScrollableMenuItem extends React.Component {this.props.text}
- ) + ); } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2ee39bc0d..4e1e76f39 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -513,7 +513,7 @@ export class CollectionDockingView extends React.Component ((view: Opt) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), (views) => { diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 79b5d7bb7..f124fe21b 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -157,6 +157,8 @@ export class CollectionSchemaCell extends React.Component { Document: this.props.rowProps.original, DataDoc: this.props.rowProps.original, LibraryPath: [], + dropAction: "alias", + bringToFront: emptyFunction, fieldKey: this.props.rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index a4502cced..981438513 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -51,8 +51,7 @@ const columnTypes: Map = new Map([ @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { - private _mainCont?: HTMLDivElement; - private _startPreviewWidth = 0; + private _previewCont?: HTMLDivElement; private DIVIDER_WIDTH = 4; @observable previewDoc: Doc | undefined = undefined; @@ -64,7 +63,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } private createTarget = (ele: HTMLDivElement) => { - this._mainCont = ele; + this._previewCont = ele; super.CreateDropTarget(ele); } @@ -81,12 +80,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } onDividerDown = (e: React.PointerEvent) => { - this._startPreviewWidth = this.previewWidth(); setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); } @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - const nativeWidth = this._mainCont!.getBoundingClientRect(); + const nativeWidth = this._previewCont!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 086e0842e..076dd3629 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -106,7 +106,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { fields.delete(header); sectionHeaders.splice(sectionHeaders.indexOf(header), 1); changed = true; - }) + }); } changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0); return fields; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 646b433bf..0a48c95e4 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -131,7 +131,8 @@ export class CollectionStackingViewFieldColumn extends React.Component NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; - return this.props.parent.props.addDocument(newDoc); + this.props.parent.props.addDocument(newDoc); + return false; } @action diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 8c7e113b2..5b9a69bf7 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -1,22 +1,20 @@ import { CollectionSubView } from "./CollectionSubView"; -import { Transform } from "../../util/Transform"; import React = require("react"); import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx"; -import { Doc } from "../../../new_fields/Doc"; import { NumCast } from "../../../new_fields/Types"; import "./CollectionStaffView.scss"; import { observer } from "mobx-react"; @observer export class CollectionStaffView extends CollectionSubView(doc => doc) { - private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(0, -this._mainCont.current!.scrollTop); - private _mainCont = React.createRef(); private _reactionDisposer: IReactionDisposer | undefined; @observable private _staves = NumCast(this.props.Document.staves); + componentWillUnmount() { + this._reactionDisposer?.(); + } componentDidMount = () => { - this._reactionDisposer = reaction( - () => NumCast(this.props.Document.staves), + this._reactionDisposer = reaction(() => NumCast(this.props.Document.staves), (staves) => runInAction(() => this._staves = staves) ); @@ -47,7 +45,7 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) { } render() { - return
+ return
{this.staves} {this.addStaffButton}
; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 88cfde0b6..70927cf22 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,5 +1,4 @@ import { action, computed, IReactionDisposer, reaction } from "mobx"; -import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -107,7 +106,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)).filter(pair => pair.layout); - return validPairs.map(({ data, layout }) => ({ data, layout: layout! })); // this mapping is a bit of a hack to coerce types + return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { return Cast(this.dataField, listSpec(Doc)); diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 64832506b..0d2207b27 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -146,7 +146,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { back -
+
; } render() { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index edb9fd930..b6ce2f3a9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -299,7 +299,7 @@ export class CollectionView extends Touchable { get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); - return validPairs.map(({ data, layout }) => ({ data, layout: layout! })); // this mapping is a bit of a hack to coerce types + return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { return Cast(this.dataField, listSpec(Doc)); diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 960c6554e..2d565d9db 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -267,10 +267,10 @@ export class CollectionViewBaseChrome extends React.Component); - case CollectionViewType.Schema: return (); - case CollectionViewType.Tree: return (); - case CollectionViewType.Masonry: return (); + case CollectionViewType.Stacking: return (); + case CollectionViewType.Schema: return (); + case CollectionViewType.Tree: return (); + case CollectionViewType.Masonry: return (); default: return null; } } @@ -355,7 +355,7 @@ export class CollectionViewBaseChrome extends React.Component Doc.setChildLayout(this.target, source?.[0]), initialize: emptyFunction, }; - DragManager.StartButtonDrag([this._viewRef.current!], c.script, c.title, + DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY); return true; }, emptyFunction, emptyFunction); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index a1ec0daef..49ca024a2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed, IReactionDisposer } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; @@ -10,67 +10,9 @@ import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; import { SelectionManager } from "../../../util/SelectionManager"; import { DocumentType } from "../../../documents/DocumentTypes"; -import { StrCast } from "../../../../new_fields/Types"; @observer export class CollectionFreeFormLinksView extends React.Component { - - _brushReactionDisposer?: IReactionDisposer; - componentDidMount() { - // this._brushReactionDisposer = reaction( - // () => { - // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; - // }, - // () => { - // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); - // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; - // views.forEach((dstDoc, i) => { - // views.forEach((srcDoc, j) => { - // let dstTarg = dstDoc; - // let srcTarg = srcDoc; - // let x1 = NumCast(srcDoc.x); - // let x2 = NumCast(dstDoc.x); - // let x1w = NumCast(srcDoc.width, -1); - // let x2w = NumCast(dstDoc.width, -1); - // if (x1w < 0 || x2w < 0 || i === j) { } - // else { - // let findBrush = (field: (Doc | Promise)[]) => field.findIndex(brush => { - // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; - // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; - // }); - // let brushAction = (field: (Doc | Promise)[]) => { - // let found = findBrush(field); - // if (found !== -1) { - // field.splice(found, 1); - // } - // }; - // if (Math.abs(x1 + x1w - x2) < 20) { - // let linkDoc: Doc = new Doc(); - // linkDoc.title = "Histogram Brush"; - // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); - // linkDoc.brushingDocs = new List([dstTarg, srcTarg]); - - // brushAction = (field: (Doc | Promise)[]) => { - // if (findBrush(field) === -1) { - // field.push(linkDoc); - // } - // }; - // } - // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List(); - // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List(); - // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); - // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); - // brushAction(dstBrushDocs); - // brushAction(srcBrushDocs); - // } - // }); - // }); - // }); - } - componentWillUnmount() { - this._brushReactionDisposer && this._brushReactionDisposer(); - } @computed get uniqueConnections() { const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { @@ -101,4 +43,60 @@ export class CollectionFreeFormLinksView extends React.Component { {this.props.children}
; } + // _brushReactionDisposer?: IReactionDisposer; + // componentDidMount() { + // this._brushReactionDisposer = reaction( + // () => { + // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; + // }, + // () => { + // let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; + // views.forEach((dstDoc, i) => { + // views.forEach((srcDoc, j) => { + // let dstTarg = dstDoc; + // let srcTarg = srcDoc; + // let x1 = NumCast(srcDoc.x); + // let x2 = NumCast(dstDoc.x); + // let x1w = NumCast(srcDoc.width, -1); + // let x2w = NumCast(dstDoc.width, -1); + // if (x1w < 0 || x2w < 0 || i === j) { } + // else { + // let findBrush = (field: (Doc | Promise)[]) => field.findIndex(brush => { + // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined; + // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false; + // }); + // let brushAction = (field: (Doc | Promise)[]) => { + // let found = findBrush(field); + // if (found !== -1) { + // field.splice(found, 1); + // } + // }; + // if (Math.abs(x1 + x1w - x2) < 20) { + // let linkDoc: Doc = new Doc(); + // linkDoc.title = "Histogram Brush"; + // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title); + // linkDoc.brushingDocs = new List([dstTarg, srcTarg]); + + // brushAction = (field: (Doc | Promise)[]) => { + // if (findBrush(field) === -1) { + // field.push(linkDoc); + // } + // }; + // } + // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List(); + // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List(); + // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []); + // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []); + // brushAction(dstBrushDocs); + // brushAction(srcBrushDocs); + // } + // }); + // }); + // }); + // } + // componentWillUnmount() { + // this._brushReactionDisposer?.(); + // } } \ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 0f94bffd6..276a49570 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -4,7 +4,7 @@ import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_ import { InkField, InkData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { Cast, NumCast, FieldValue } from "../../../../new_fields/Types"; +import { Cast, NumCast, FieldValue, StrCast } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -107,7 +107,7 @@ export class MarqueeView extends React.Component(Butt style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
{(this.Document.text || this.Document.title)} diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 8632f9c9a..9494a4bc4 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -22,6 +22,7 @@ interface ContentFittingDocumentViewProps { childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; + layoutKey?: string; dropAction?: dropActionType; PanelWidth: () => number; PanelHeight: () => number; @@ -88,6 +89,7 @@ export class ContentFittingDocumentView extends React.Componentawaiting layout

"; const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); - if (layout === undefined) { - return this.props.Document.data ? - "" : - KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : ""); - } else if (typeof layout === "string") { - return layout; - } else { - return "

Loading layout

"; - } + if (this.props.layoutKey === "layout_keyValue") { + return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data")); + } else + if (layout === undefined) { + return this.props.Document.data ? + "" : + KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : ""); + } else if (typeof layout === "string") { + return layout; + } else { + return "

Loading layout

"; + } } get dataDoc() { @@ -81,7 +84,8 @@ export class DocumentContentsView extends React.Component { fieldKey: this.props.keyName, isSelected: returnFalse, select: emptyFunction, + dropAction:"alias", + bringToFront:emptyFunction, renderDepth: 1, active: returnFalse, whenActiveChanged: emptyFunction, diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx index fdc732d3f..bd5b3bff4 100644 --- a/src/client/views/nodes/RadialMenuItem.tsx +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -44,12 +44,12 @@ export class RadialMenuItem extends React.Component { setcircle() { let circlemin = 0; - let circlemax = 1 + let circlemax = 1; this.props.min ? circlemin = this.props.min : null; this.props.max ? circlemax = this.props.max : null; if (document.getElementById("myCanvas") !== null) { - var c: any = document.getElementById("myCanvas"); - let color = "white" + const c: any = document.getElementById("myCanvas"); + let color = "white"; switch (circlemin % 3) { case 1: color = "#c2c2c5"; @@ -70,38 +70,38 @@ export class RadialMenuItem extends React.Component { } if (c.getContext) { - var ctx = c.getContext("2d"); + const ctx = c.getContext("2d"); ctx.beginPath(); ctx.arc(150, 150, 150, (circlemin / circlemax) * 2 * Math.PI, ((circlemin + 1) / circlemax) * 2 * Math.PI); ctx.arc(150, 150, 50, ((circlemin + 1) / circlemax) * 2 * Math.PI, (circlemin / circlemax) * 2 * Math.PI, true); ctx.fillStyle = color; - ctx.fill() + ctx.fill(); } } } calculatorx() { let circlemin = 0; - let circlemax = 1 + let circlemax = 1; this.props.min ? circlemin = this.props.min : null; this.props.max ? circlemax = this.props.max : null; - let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; - let degrees = 360 * avg; - let x = 100 * Math.cos(degrees * Math.PI / 180); - let y = -125 * Math.sin(degrees * Math.PI / 180); + const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + const degrees = 360 * avg; + const x = 100 * Math.cos(degrees * Math.PI / 180); + const y = -125 * Math.sin(degrees * Math.PI / 180); return x; } calculatory() { let circlemin = 0; - let circlemax = 1 + let circlemax = 1; this.props.min ? circlemin = this.props.min : null; this.props.max ? circlemax = this.props.max : null; - let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; - let degrees = 360 * avg; - let x = 125 * Math.cos(degrees * Math.PI / 180); - let y = -100 * Math.sin(degrees * Math.PI / 180); + const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + const degrees = 360 * avg; + const x = 125 * Math.cos(degrees * Math.PI / 180); + const y = -100 * Math.sin(degrees * Math.PI / 180); return y; } diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 548066f1c..7c58a5148 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -142,7 +142,7 @@ export class ScreenshotBox extends DocAnnotatableComponent { this._screenCapture = !this._screenCapture; this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - }) + }); private get uIButtons() { return (
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 591864f2c..838fbefb1 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -63,7 +63,7 @@ export class WebBox extends DocAnnotatableComponent this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } else if (field?.url) { - var result = await WebRequest.get(Utils.CorsProxy(field.url.href)); + const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); this.dataDoc.text = htmlToText.fromString(result.content); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index a1e7d5c2a..cc187cd67 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -238,7 +238,7 @@ export class PDFViewer extends DocAnnotatableComponent { if (e.keyCode === 13) { - let submittedTitle = this.roomText!.value; + const submittedTitle = this.roomText!.value; this.roomText!.value = ""; this.roomText!.blur(); initialize(submittedTitle, this.changeUILook); @@ -56,7 +56,7 @@ export class DashWebRTCVideo extends React.Component
DashWebRTC
this.roomText = e!} onKeyDown={this.onEnterKeyDown} /> @@ -72,8 +72,8 @@ export class DashWebRTCVideo extends React.Component
; - let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 5d3a517ae..1d2d57b96 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -199,6 +199,8 @@ export default class MobileInterface extends React.Component { DataDoc={undefined} LibraryPath={emptyPath} fieldKey={""} + dropAction={"alias"} + bringToFront={emptyFunction } addDocTab={returnFalse} pinToPres={emptyFunction} PanelHeight={() => window.innerHeight} diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 25b526168..440f13d6b 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -353,7 +353,7 @@ export namespace Doc { // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). export function GetProto(doc: Doc): Doc { if (doc instanceof Promise) { - console.log("GetProto: error: got Promise insead of Doc") + console.log("GetProto: error: got Promise insead of Doc"); } const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc)); return proto === doc ? proto : Doc.GetProto(proto); @@ -697,7 +697,7 @@ export namespace Doc { // the document containing the view layout information - will be the Document itself unless the Document has // a layout field or 'layout' is given. export function Layout(doc: Doc, layout?: Doc): Doc { - const overrideLayout = layout && Cast(doc["data-layout[" + layout[Id] + "]"], Doc, null); + const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null); return overrideLayout || doc[LayoutSym] || doc; } export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; } diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 3a1fd41f8..8c719ccd8 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -12,7 +12,7 @@ function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -let tracing = false; +const tracing = false; export function TraceMobx() { tracing && trace(); } diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts index 46c9e22ed..12064668b 100644 --- a/src/server/DashSession/Session/agents/applied_session_agent.ts +++ b/src/server/DashSession/Session/agents/applied_session_agent.ts @@ -44,7 +44,7 @@ export abstract class AppliedSessionAgent { if (!this.launched) { this.launched = true; if (isMaster) { - this.sessionMonitorRef = Monitor.Create() + this.sessionMonitorRef = Monitor.Create(); const sessionKey = await this.initializeMonitor(this.sessionMonitorRef); this.sessionMonitorRef.finalize(sessionKey); } else { diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts index 6f8d25614..ee8afee65 100644 --- a/src/server/DashSession/Session/agents/monitor.ts +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -167,7 +167,7 @@ export class Monitor extends IPCMessageReceiver { * and pass down any variables the pertinent to the child processes as environment variables. */ private loadAndValidateConfiguration = (): Configuration => { - let config: Configuration; + let config: Configuration | undefined; try { console.log(this.timestamp(), cyan("validating configuration...")); config = JSON.parse(readFileSync('./session.config.json', 'utf8')); diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts index 9f0db8330..feff568e1 100644 --- a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -35,8 +35,8 @@ export type MessageHandler = (args: T) => (any | Promise); * When a message is emitted, it is embedded with private metadata * to facilitate the resolution of promises, etc. */ -interface InternalMessage extends Message { metadata: Metadata } -interface Metadata { isResponse: boolean; id: string } +interface InternalMessage extends Message { metadata: Metadata; } +interface Metadata { isResponse: boolean; id: string; } type InternalMessageHandler = (message: InternalMessage) => (any | Promise); /** @@ -133,7 +133,7 @@ export class PromisifiedIPCManager { Object.keys(pendingMessages).forEach(id => { const error: ErrorLike = { name: "ManagerDestroyed", message: "The IPC manager was destroyed before the response could be returned." }; const message: InternalMessage = { name: pendingMessages[id], args: { error }, metadata: { id, isResponse: true } }; - this.target.send?.(message) + this.target.send?.(message); }); this.pendingMessages = {}; } diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index c5dc22912..9f9fc9619 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -55,8 +55,8 @@ export namespace WebSocket { socket.on('create or join', function (room) { console.log('Received request to create or join room ' + room); - var clientsInRoom = socket.adapter.rooms[room]; - var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; + const clientsInRoom = socket.adapter.rooms[room]; + const numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); if (numClients === 0) { @@ -76,8 +76,8 @@ export namespace WebSocket { }); socket.on('ipaddr', function () { - var ifaces = networkInterfaces(); - for (var dev in ifaces) { + const ifaces = networkInterfaces(); + for (const dev in ifaces) { ifaces[dev].forEach(function (details) { if (details.family === 'IPv4' && details.address !== '127.0.0.1') { socket.emit('ipaddr', details.address); diff --git a/src/server/database.ts b/src/server/database.ts index 055f04c49..fc91ff3a2 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -328,7 +328,7 @@ export namespace Database { export const LogUpload = async (information: Upload.ImageInformation) => { const bundle = { - _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)), + _id: Utils.GenerateDeterministicGuid(String(information.contentSize)), ...information }; return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory); diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 7b2228831..1150118f7 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -123,7 +123,7 @@ function registerCorsProxy(server: express.Express) { const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; server.use("/corsProxy", (req, res) => { - let requrl = decodeURIComponent(req.url.substring(1)); + const requrl = decodeURIComponent(req.url.substring(1)); const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ""; // cors weirdness here... // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/ and the requested url path was relative, -- cgit v1.2.3-70-g09d2