Skip to content
Snippets Groups Projects
Commit a1359477 authored by David Sehnal's avatar David Sehnal
Browse files

mol-state & plugin: basic logging

parent 74251c04
Branches
No related tags found
No related merge requests found
...@@ -17,6 +17,7 @@ import { PluginBehaviors, BuiltInPluginBehaviors } from './behavior'; ...@@ -17,6 +17,7 @@ import { PluginBehaviors, BuiltInPluginBehaviors } from './behavior';
import { Loci, EmptyLoci } from 'mol-model/loci'; import { Loci, EmptyLoci } from 'mol-model/loci';
import { Representation } from 'mol-repr'; import { Representation } from 'mol-repr';
import { CreateStructureFromPDBe } from './state/actions/basic'; import { CreateStructureFromPDBe } from './state/actions/basic';
import { LogEntry } from 'mol-util/log-entry';
export class PluginContext { export class PluginContext {
private disposed = false; private disposed = false;
...@@ -31,7 +32,8 @@ export class PluginContext { ...@@ -31,7 +32,8 @@ export class PluginContext {
behavior: this.state.behavior.events, behavior: this.state.behavior.events,
cameraSnapshots: this.state.cameraSnapshots.events, cameraSnapshots: this.state.cameraSnapshots.events,
snapshots: this.state.snapshots.events, snapshots: this.state.snapshots.events,
} },
log: this.ev<LogEntry>()
}; };
readonly behaviors = { readonly behaviors = {
...@@ -61,6 +63,10 @@ export class PluginContext { ...@@ -61,6 +63,10 @@ export class PluginContext {
} }
} }
log(e: LogEntry) {
this.events.log.next(e);
}
/** /**
* This should be used in all transform related request so that it could be "spoofed" to allow * This should be used in all transform related request so that it could be "spoofed" to allow
* "static" access to resources. * "static" access to resources.
...@@ -87,6 +93,8 @@ export class PluginContext { ...@@ -87,6 +93,8 @@ export class PluginContext {
BuiltInPluginBehaviors.State.registerDefault(this); BuiltInPluginBehaviors.State.registerDefault(this);
BuiltInPluginBehaviors.Representation.registerDefault(this); BuiltInPluginBehaviors.Representation.registerDefault(this);
BuiltInPluginBehaviors.Camera.registerDefault(this); BuiltInPluginBehaviors.Camera.registerDefault(this);
merge(this.state.data.events.log, this.state.behavior.events.log).subscribe(e => this.events.log.next(e));
} }
async _test_initBehaviors() { async _test_initBehaviors() {
......
...@@ -21,7 +21,7 @@ export class Controls extends PluginComponent<{ }, { }> { ...@@ -21,7 +21,7 @@ export class Controls extends PluginComponent<{ }, { }> {
} }
export class _test_TrajectoryControls extends PluginComponent { export class TrajectoryControls extends PluginComponent {
render() { render() {
return <div> return <div>
<b>Trajectory: </b> <b>Trajectory: </b>
......
...@@ -8,12 +8,15 @@ import * as React from 'react'; ...@@ -8,12 +8,15 @@ import * as React from 'react';
import { PluginContext } from '../context'; import { PluginContext } from '../context';
import { StateTree } from './state-tree'; import { StateTree } from './state-tree';
import { Viewport, ViewportControls } from './viewport'; import { Viewport, ViewportControls } from './viewport';
import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls } from './controls'; import { Controls, _test_UpdateTransform, _test_ApplyAction, TrajectoryControls } from './controls';
import { PluginComponent, PluginReactContext } from './base'; import { PluginComponent, PluginReactContext } from './base';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { State } from 'mol-state'; import { State } from 'mol-state';
import { CameraSnapshots } from './camera'; import { CameraSnapshots } from './camera';
import { StateSnapshots } from './state'; import { StateSnapshots } from './state';
import { List } from 'immutable';
import { LogEntry } from 'mol-util/log-entry';
import { formatTime } from 'mol-util';
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
render() { render() {
...@@ -24,15 +27,15 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { ...@@ -24,15 +27,15 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
<h3>Behaviors</h3> <h3>Behaviors</h3>
<StateTree state={this.props.plugin.state.behavior} /> <StateTree state={this.props.plugin.state.behavior} />
</div> </div>
<div style={{ position: 'absolute', left: '350px', right: '300px', height: '100%' }}> <div style={{ position: 'absolute', left: '350px', right: '300px', top: '0', bottom: '100px' }}>
<Viewport /> <Viewport />
<div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}> <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}>
<_test_TrajectoryControls /> <TrajectoryControls />
</div> </div>
<ViewportControls /> <ViewportControls />
</div> </div>
<div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px', overflowY: 'scroll' }}> <div style={{ position: 'absolute', width: '300px', right: '0', top: '0', padding: '10px', overflowY: 'scroll' }}>
<_test_CurrentObject /> <CurrentObject />
<hr /> <hr />
<Controls /> <Controls />
<hr /> <hr />
...@@ -40,12 +43,44 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { ...@@ -40,12 +43,44 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
<hr /> <hr />
<StateSnapshots /> <StateSnapshots />
</div> </div>
<div style={{ position: 'absolute', right: '300px', left: '350px', bottom: '0', height: '100px', overflow: 'hidden' }}>
<Log />
</div>
</div> </div>
</PluginReactContext.Provider>; </PluginReactContext.Provider>;
} }
} }
export class _test_CurrentObject extends PluginComponent { export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
private wrapper = React.createRef<HTMLDivElement>();
componentDidMount() {
this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) }));
}
componentDidUpdate() {
this.scrollToBottom();
}
state = { entries: List<LogEntry>() };
private scrollToBottom() {
const log = this.wrapper.current;
if (log) log.scrollTop = log.scrollHeight - log.clientHeight - 1;
}
render() {
return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', padding: '10px', overflowY: 'scroll' }}>
<ul style={{ listStyle: 'none' }}>
{this.state.entries.map((e, i) => <li key={i} style={{ borderBottom: '1px solid #999', padding: '3px' }}>
[{e!.type}] [{formatTime(e!.timestamp)}] {e!.message}
</li>)}
</ul>
</div>;
}
}
export class CurrentObject extends PluginComponent {
componentDidMount() { componentDidMount() {
let current: State.ObjectEvent | undefined = void 0; let current: State.ObjectEvent | undefined = void 0;
this.subscribe(merge(this.plugin.behaviors.state.data.currentObject, this.plugin.behaviors.state.behavior.currentObject), o => { this.subscribe(merge(this.plugin.behaviors.state.data.currentObject, this.plugin.behaviors.state.behavior.currentObject), o => {
......
...@@ -16,6 +16,8 @@ import { StateTreeBuilder } from './tree/builder'; ...@@ -16,6 +16,8 @@ import { StateTreeBuilder } from './tree/builder';
import { StateAction } from './action'; import { StateAction } from './action';
import { StateActionManager } from './action/manager'; import { StateActionManager } from './action/manager';
import { TransientTree } from './tree/transient'; import { TransientTree } from './tree/transient';
import { LogEntry } from 'mol-util/log-entry';
import { now, formatTimespan } from 'mol-util/now';
export { State } export { State }
...@@ -37,7 +39,7 @@ class State { ...@@ -37,7 +39,7 @@ class State {
created: this.ev<State.ObjectEvent & { obj: StateObject }>(), created: this.ev<State.ObjectEvent & { obj: StateObject }>(),
removed: this.ev<State.ObjectEvent & { obj?: StateObject }>() removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
}, },
warn: this.ev<string>(), log: this.ev<LogEntry>(),
changed: this.ev<void>() changed: this.ev<void>()
}; };
...@@ -369,6 +371,7 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { ...@@ -369,6 +371,7 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) {
if (errorText) { if (errorText) {
setCellStatus(ctx, ref, 'error', errorText); setCellStatus(ctx, ref, 'error', errorText);
ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText });
} }
const cell = ctx.cells.get(ref)!; const cell = ctx.cells.get(ref)!;
...@@ -391,27 +394,33 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { ...@@ -391,27 +394,33 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) {
type UpdateNodeResult = type UpdateNodeResult =
| { action: 'created', obj: StateObject } | { action: 'created', obj: StateObject }
| { action: 'updated', obj: StateObject } | { action: 'updated', obj: StateObject }
| { action: 'replaced', oldObj?: StateObject, newObj: StateObject } | { action: 'replaced', oldObj?: StateObject, obj: StateObject }
| { action: 'none' } | { action: 'none' }
async function updateSubtree(ctx: UpdateContext, root: Ref) { async function updateSubtree(ctx: UpdateContext, root: Ref) {
setCellStatus(ctx, root, 'processing'); setCellStatus(ctx, root, 'processing');
try { try {
const start = now();
const update = await updateNode(ctx, root); const update = await updateNode(ctx, root);
const time = now() - start;
if (update.action !== 'none') ctx.changed = true; if (update.action !== 'none') ctx.changed = true;
setCellStatus(ctx, root, 'ok'); setCellStatus(ctx, root, 'ok');
if (update.action === 'created') { if (update.action === 'created') {
ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! }); ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! });
ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
if (!ctx.hadError) { if (!ctx.hadError) {
const transform = ctx.tree.transforms.get(root); const transform = ctx.tree.transforms.get(root);
if (!transform.props || !transform.props.isGhost) ctx.newCurrent = root; if (!transform.props || !transform.props.isGhost) ctx.newCurrent = root;
} }
} else if (update.action === 'updated') { } else if (update.action === 'updated') {
ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'in-place', obj: update.obj }); ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'in-place', obj: update.obj });
ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
} else if (update.action === 'replaced') { } else if (update.action === 'replaced') {
ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'recreate', obj: update.newObj, oldObj: update.oldObj }); ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, action: 'recreate', obj: update.obj, oldObj: update.oldObj });
ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
} }
} catch (e) { } catch (e) {
ctx.changed = true; ctx.changed = true;
...@@ -463,7 +472,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo ...@@ -463,7 +472,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
const newObj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); const newObj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
current.obj = newObj; current.obj = newObj;
current.version = transform.version; current.version = transform.version;
return { action: 'replaced', oldObj, newObj: newObj }; return { action: 'replaced', oldObj, obj: newObj };
} }
case Transformer.UpdateResult.Updated: case Transformer.UpdateResult.Updated:
current.version = transform.version; current.version = transform.version;
......
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
export { LogEntry }
interface LogEntry {
type: LogEntry.Type,
timestamp: Date,
message: string
}
namespace LogEntry {
export type Type = 'message' | 'error' | 'warning' | 'info'
export function message(msg: string): LogEntry { return { type: 'message', timestamp: new Date(), message: msg }; }
export function error(msg: string): LogEntry { return { type: 'error', timestamp: new Date(), message: msg }; }
export function warning(msg: string): LogEntry { return { type: 'warning', timestamp: new Date(), message: msg }; }
export function info(msg: string): LogEntry { return { type: 'info', timestamp: new Date(), message: msg }; }
}
\ No newline at end of file
...@@ -27,4 +27,21 @@ namespace now { ...@@ -27,4 +27,21 @@ namespace now {
export type Timestamp = number & { '@type': 'now-timestamp' } export type Timestamp = number & { '@type': 'now-timestamp' }
} }
export { now }
\ No newline at end of file function formatTimespan(t: number) {
if (isNaN(t)) return 'n/a';
let h = Math.floor(t / (60 * 60 * 1000)),
m = Math.floor(t / (60 * 1000) % 60),
s = Math.floor(t / 1000 % 60),
ms = Math.floor(t % 1000).toString();
while (ms.length < 3) ms = '0' + ms;
if (h > 0) return `${h}h${m}m${s}.${ms}s`;
if (m > 0) return `${m}m${s}.${ms}s`;
if (s > 0) return `${s}.${ms}s`;
return `${t.toFixed(0)}ms`;
}
export { now, formatTimespan }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment