diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 10014d4da34a0f6b175eba8d7df49271e6263e7b..4c4124f913246b77ee1a7e0356baa2585dfe07ca 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -72,4 +72,20 @@ export function Snapshots(ctx: PluginContext) { const e = ctx.state.snapshots.getEntry(id); return ctx.state.setSnapshot(e.snapshot); }); + + PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, serverUrl }) => { + return fetch(`${serverUrl}/set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`, { + method: 'POST', + mode: 'cors', + referrer: 'no-referrer', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: JSON.stringify(ctx.state.getSnapshot()) + }) as any as Promise<void>; + }); + + PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => { + const req = await fetch(url, { referrer: 'no-referrer' }); + const json = await req.json(); + return ctx.state.setSnapshot(json.data); + }); } \ No newline at end of file diff --git a/src/mol-plugin/command/state.ts b/src/mol-plugin/command/state.ts index ed9ca61c78bf5c0ee356a3f2fd8342f94ec96715..e9b092b00e04e65e1c910d6bde8c73325de761f7 100644 --- a/src/mol-plugin/command/state.ts +++ b/src/mol-plugin/command/state.ts @@ -25,4 +25,7 @@ export const Snapshots = { Remove: PluginCommand<{ id: string }>({ isImmediate: true }), Apply: PluginCommand<{ id: string }>({ isImmediate: true }), Clear: PluginCommand<{ }>({ isImmediate: true }), + + Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }), + Fetch: PluginCommand<{ url: string }>() } \ No newline at end of file diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index f54fd023beeac8facb7b407428a9a7f352c82722..491b65dfe9fcca732808ddb8d9dca47513ed5164 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -8,6 +8,7 @@ import { PluginContext } from './context'; import { Plugin } from './ui/plugin' import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { PluginCommands } from './command'; function getParam(name: string, regex: string): string { let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); @@ -28,8 +29,9 @@ export function createPlugin(target: HTMLElement): PluginContext { } function trySetSnapshot(ctx: PluginContext) { - const snapshot = getParam('snapshot', `(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?`); - if (!snapshot) return; - const data = JSON.parse(atob(snapshot)); - setTimeout(() => ctx.state.setSnapshot(data), 250); + const snapshotUrl = getParam('snapshot-url', `[^&]+`); + if (!snapshotUrl) return; + // const data = JSON.parse(atob(snapshot)); + // setTimeout(() => ctx.state.setSnapshot(data), 250); + PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl }) } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index d424c837d432d3d9fd096dfb5af9935fa53411c5..959829a78b73272e5b4aff7e4a54e484ad47a0f6 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -38,7 +38,7 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { <BackgroundTaskProgress /> </div> </div> - <div style={{ position: 'absolute', width: '300px', right: '0', top: '0', padding: '10px', overflowY: 'scroll' }}> + <div style={{ position: 'absolute', width: '300px', right: '0', top: '0', bottom: '0', padding: '10px', overflowY: 'scroll' }}> <CurrentObject /> <hr /> <Controls /> diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index 40ad8905684573a0678dccf4d16e2ddf88129726..10ce5b866bf2d4989123cf656939370273997a2b 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/src/mol-plugin/ui/state.tsx @@ -7,22 +7,31 @@ import { PluginCommands } from 'mol-plugin/command'; import * as React from 'react'; import { PluginComponent } from './base'; +import { shallowEqual } from 'mol-util'; +import { List } from 'immutable'; +import { LogEntry } from 'mol-util/log-entry'; + +export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { + state = { serverUrl: 'http://webchem.ncbr.muni.cz/molstar-state' } + + updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) }; -export class StateSnapshots extends PluginComponent<{ }, { }> { render() { return <div> <h3>State Snapshots</h3> - <StateSnapshotControls /> - <StateSnapshotList /> + <StateSnapshotControls serverUrl={this.state.serverUrl} serverChanged={this.updateServerUrl} /> + <b>Local</b> + <LocalStateSnapshotList /> + <RemoteStateSnapshotList serverUrl={this.state.serverUrl} /> </div>; } } -class StateSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { - state = { name: '', description: '' }; +class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { + state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false }; add = () => { - PluginCommands.State.Snapshots.Add.dispatch(this.plugin, this.state); + PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.name, description: this.state.description }); this.setState({ name: '', description: '' }) } @@ -30,17 +39,32 @@ class StateSnapshotControls extends PluginComponent<{ }, { name: string, descrip PluginCommands.State.Snapshots.Clear.dispatch(this.plugin, {}); } + shouldComponentUpdate(nextProps: { serverUrl: string, serverChanged: (url: string) => void }, nextState: { name: string, description: string, serverUrl: string, isUploading: boolean }) { + return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); + } + + upload = async () => { + this.setState({ isUploading: true }); + await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl }); + this.setState({ isUploading: false }); + } + render() { return <div> <input type='text' value={this.state.name} placeholder='Name...' style={{ width: '33%', display: 'block', float: 'left' }} onChange={e => this.setState({ name: e.target.value })} /> <input type='text' value={this.state.description} placeholder='Description...' style={{ width: '67%', display: 'block' }} onChange={e => this.setState({ description: e.target.value })} /> + <input type='text' value={this.state.serverUrl} placeholder='Server URL...' style={{ width: '100%', display: 'block' }} onChange={e => { + this.setState({ serverUrl: e.target.value }); + this.props.serverChanged(e.target.value); + }} /> <button style={{ float: 'right' }} onClick={this.clear}>Clear</button> - <button onClick={this.add}>Add</button> + <button onClick={this.add}>Add Local</button> + <button onClick={this.upload} disabled={this.state.isUploading}>Upload</button> </div>; } } -class StateSnapshotList extends PluginComponent<{ }, { }> { +class LocalStateSnapshotList extends PluginComponent<{ }, { }> { componentDidMount() { this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); } @@ -64,4 +88,42 @@ class StateSnapshotList extends PluginComponent<{ }, { }> { </li>)} </ul>; } +} + +type RemoteEntry = { url: string, timestamp: number, id: string, name: string, description: string } +class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { + state = { entries: List<RemoteEntry>(), isFetching: false }; + + componentDidMount() { + this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); + this.refresh(); + } + + refresh = async () => { + try { + this.setState({ isFetching: true }); + const req = await fetch(`${this.props.serverUrl}/list`); + const json: RemoteEntry[] = await req.json(); + this.setState({ entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({ ...e, url: `${this.props.serverUrl}/get/${e.id}` }))), isFetching: false }) + } catch (e) { + this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e)); + this.setState({ entries: List<RemoteEntry>(), isFetching: false }) + } + } + + fetch(url: string) { + return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url }); + } + + render() { + return <div> + <b>Remote</b> <button onClick={this.refresh} disabled={this.state.isFetching}>Refresh</button> + <ul style={{ listStyle: 'none' }}> + {this.state.entries.valueSeq().map(e =><li key={e!.id}> + <button onClick={this.fetch(e!.url)} disabled={this.state.isFetching}>Fetch</button> + {e!.name} <small>{e!.description}</small> + </li>)} + </ul> + </div>; + } } \ No newline at end of file