|
@@ -6,63 +6,63 @@
|
|
|
|
|
|
import { PluginCommands } from 'mol-plugin/command';
|
|
|
import * as React from 'react';
|
|
|
-import { PluginUIComponent } from './base';
|
|
|
+import { PluginUIComponent, PurePluginUIComponent } from './base';
|
|
|
import { shallowEqual } from 'mol-util';
|
|
|
-import { List } from 'immutable';
|
|
|
+import { OrderedMap } from 'immutable';
|
|
|
import { ParameterControls } from './controls/parameters';
|
|
|
import { ParamDefinition as PD} from 'mol-util/param-definition';
|
|
|
-import { Subject } from 'rxjs';
|
|
|
+import { PluginState } from 'mol-plugin/state';
|
|
|
+import { urlCombine } from 'mol-util/url';
|
|
|
|
|
|
-export class StateSnapshots extends PluginUIComponent<{ }, { serverUrl: string }> {
|
|
|
- state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' }
|
|
|
-
|
|
|
- updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
|
|
|
+export class StateSnapshots extends PluginUIComponent<{ }> {
|
|
|
|
|
|
render() {
|
|
|
return <div>
|
|
|
- <div className='msp-section-header'>State Snapshots</div>
|
|
|
- <StateSnapshotControls serverUrl={this.state.serverUrl} serverChanged={this.updateServerUrl} />
|
|
|
+ <div className='msp-section-header'>State</div>
|
|
|
+ <StateSnapshotControls />
|
|
|
<LocalStateSnapshotList />
|
|
|
- <RemoteStateSnapshotList serverUrl={this.state.serverUrl} />
|
|
|
+ <RemoteStateSnapshots />
|
|
|
</div>;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// TODO: this is not nice: device some custom event system.
|
|
|
-const UploadedEvent = new Subject();
|
|
|
+class StateSnapshotControls extends PluginUIComponent<
|
|
|
+ { },
|
|
|
+ { params: PD.Values<typeof StateSnapshotControls.Params> }> {
|
|
|
|
|
|
-class StateSnapshotControls extends PluginUIComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> {
|
|
|
- state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
|
|
|
+ state = { params: PD.getDefaultValues(StateSnapshotControls.Params) };
|
|
|
|
|
|
static Params = {
|
|
|
name: PD.Text(),
|
|
|
- description: PD.Text(),
|
|
|
- serverUrl: PD.Text()
|
|
|
- }
|
|
|
+ options: PD.Group({
|
|
|
+ description: PD.Text(),
|
|
|
+ ...PluginState.GetSnapshotParams
|
|
|
+ })
|
|
|
+ };
|
|
|
|
|
|
add = () => {
|
|
|
- PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.name, description: this.state.description });
|
|
|
- this.setState({ name: '', description: '' })
|
|
|
+ PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.params.name, description: this.state.params.options.description });
|
|
|
+ this.setState({
|
|
|
+ params: {
|
|
|
+ name: '',
|
|
|
+ options: {
|
|
|
+ ...this.state.params.options,
|
|
|
+ description: ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
clear = () => {
|
|
|
PluginCommands.State.Snapshots.Clear.dispatch(this.plugin, {});
|
|
|
}
|
|
|
|
|
|
- shouldComponentUpdate(nextProps: { serverUrl: string, serverChanged: (url: string) => void }, nextState: { name: string, description: string, serverUrl: string, isUploading: boolean }) {
|
|
|
+ shouldComponentUpdate(nextProps: any, nextState: any) {
|
|
|
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 });
|
|
|
- this.plugin.log.message('Snapshot uploaded.');
|
|
|
- UploadedEvent.next();
|
|
|
- }
|
|
|
-
|
|
|
- download = () => {
|
|
|
- PluginCommands.State.Snapshots.DownloadToFile.dispatch(this.plugin, { name: this.state.name });
|
|
|
+ downloadToFile = () => {
|
|
|
+ PluginCommands.State.Snapshots.DownloadToFile.dispatch(this.plugin, { name: this.state.params.name });
|
|
|
}
|
|
|
|
|
|
open = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -72,23 +72,24 @@ class StateSnapshotControls extends PluginUIComponent<{ serverUrl: string, serve
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
+ // TODO: proper styling
|
|
|
return <div>
|
|
|
- <ParameterControls params={StateSnapshotControls.Params} values={this.state} onEnter={this.add} onChange={p => {
|
|
|
- this.setState({ [p.name]: p.value } as any);
|
|
|
- if (p.name === 'serverUrl') this.props.serverChanged(p.value);
|
|
|
+ <div className='msp-btn-row-group' style={{ marginBottom: '10px' }}>
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' onClick={this.downloadToFile}>Download JSON</button>
|
|
|
+ <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
|
|
|
+ {'Open JSON'} <input onChange={this.open} type='file' multiple={false} accept='.json' />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ParameterControls params={StateSnapshotControls.Params} values={this.state.params} onEnter={this.add} onChange={p => {
|
|
|
+ this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
|
|
|
}}/>
|
|
|
|
|
|
<div className='msp-btn-row-group'>
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Add Local</button>
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button>
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Save</button>
|
|
|
+ {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button> */}
|
|
|
<button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
|
|
|
</div>
|
|
|
- <div className='msp-btn-row-group'>
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.download}>Download JSON</button>
|
|
|
- <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
|
|
|
- {'Open JSON'} <input onChange={this.open} type='file' multiple={false} accept='.json' />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
</div>;
|
|
|
}
|
|
|
}
|
|
@@ -109,9 +110,12 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
+ const current = this.plugin.state.snapshots.state.current;
|
|
|
return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
|
|
|
{this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.snapshot.id}>
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.snapshot.id)}>{e!.name || new Date(e!.timestamp).toLocaleString()} <small>{e!.description}</small></button>
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.snapshot.id)}>
|
|
|
+ <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>{e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>{e!.description}</small>
|
|
|
+ </button>
|
|
|
<button onClick={this.remove(e!.snapshot.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
|
|
|
<span className='msp-icon msp-icon-remove' />
|
|
|
</button>
|
|
@@ -121,59 +125,139 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
|
|
|
}
|
|
|
|
|
|
type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string }
|
|
|
-class RemoteStateSnapshotList extends PluginUIComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
|
|
|
- state = { entries: List<RemoteEntry>(), isFetching: false };
|
|
|
+class RemoteStateSnapshots extends PluginUIComponent<
|
|
|
+ { },
|
|
|
+ { params: PD.Values<typeof RemoteStateSnapshots.Params>, entries: OrderedMap<string, RemoteEntry>, isBusy: boolean }> {
|
|
|
+
|
|
|
+ state = { params: PD.getDefaultValues(RemoteStateSnapshots.Params), entries: OrderedMap<string, RemoteEntry>(), isBusy: false };
|
|
|
+
|
|
|
+ static Params = {
|
|
|
+ name: PD.Text(),
|
|
|
+ options: PD.Group({
|
|
|
+ description: PD.Text(),
|
|
|
+ serverUrl: PD.Text('https://webchem.ncbr.muni.cz/molstar-state')
|
|
|
+ })
|
|
|
+ };
|
|
|
|
|
|
componentDidMount() {
|
|
|
- this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
|
|
|
this.refresh();
|
|
|
- this.subscribe(UploadedEvent, this.refresh);
|
|
|
+ // this.subscribe(UploadedEvent, this.refresh);
|
|
|
+ }
|
|
|
+
|
|
|
+ serverUrl(q?: string) {
|
|
|
+ if (!q) return this.state.params.options.serverUrl;
|
|
|
+ return urlCombine(this.state.params.options.serverUrl, q);
|
|
|
}
|
|
|
|
|
|
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) => ({
|
|
|
+ this.setState({ isBusy: true });
|
|
|
+ const json = await this.plugin.runTask<RemoteEntry[]>(this.plugin.fetch({ url: this.serverUrl('list'), type: 'json' }));
|
|
|
+ const entries = OrderedMap<string, RemoteEntry>().asMutable();
|
|
|
+ for (const e of json) {
|
|
|
+ entries.set(e.id, {
|
|
|
...e,
|
|
|
- url: `${this.props.serverUrl}/get/${e.id}`,
|
|
|
- removeUrl: `${this.props.serverUrl}/remove/${e.id}`
|
|
|
- }))),
|
|
|
- isFetching: false })
|
|
|
+ url: this.serverUrl(`get/${e.id}`),
|
|
|
+ removeUrl: this.serverUrl(`remove/${e.id}`)
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState({ entries: entries.asImmutable(), isBusy: false })
|
|
|
} catch (e) {
|
|
|
this.plugin.log.error('Fetching Remote Snapshots: ' + e);
|
|
|
- this.setState({ entries: List<RemoteEntry>(), isFetching: false })
|
|
|
+ this.setState({ entries: OrderedMap(), isBusy: false })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- fetch(url: string) {
|
|
|
- return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url });
|
|
|
+ upload = async () => {
|
|
|
+ this.setState({ isBusy: true });
|
|
|
+ if (this.plugin.state.snapshots.state.entries.size === 0) {
|
|
|
+ await PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.params.name, description: this.state.params.options.description });
|
|
|
+ }
|
|
|
+
|
|
|
+ await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, {
|
|
|
+ name: this.state.params.name,
|
|
|
+ description: this.state.params.options.description,
|
|
|
+ serverUrl: this.state.params.options.serverUrl
|
|
|
+ });
|
|
|
+ this.setState({ isBusy: false });
|
|
|
+ this.plugin.log.message('Snapshot uploaded.');
|
|
|
+ this.refresh();
|
|
|
}
|
|
|
|
|
|
- remove(url: string) {
|
|
|
- return async () => {
|
|
|
- this.setState({ entries: List() });
|
|
|
- try {
|
|
|
- await fetch(url);
|
|
|
- } catch { }
|
|
|
- this.refresh();
|
|
|
+ fetch = async (e: React.MouseEvent<HTMLElement>) => {
|
|
|
+ const id = e.currentTarget.getAttribute('data-id');
|
|
|
+ if (!id) return;
|
|
|
+ const entry = this.state.entries.get(id);
|
|
|
+ if (!entry) return;
|
|
|
+
|
|
|
+ this.setState({ isBusy: true });
|
|
|
+ try {
|
|
|
+ await PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url: entry.url });
|
|
|
+ } finally {
|
|
|
+ this.setState({ isBusy: false });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ remove = async (e: React.MouseEvent<HTMLElement>) => {
|
|
|
+ const id = e.currentTarget.getAttribute('data-id');
|
|
|
+ if (!id) return;
|
|
|
+ const entry = this.state.entries.get(id);
|
|
|
+ if (!entry) return;
|
|
|
+ this.setState({ entries: this.state.entries.remove(id) });
|
|
|
+
|
|
|
+ try {
|
|
|
+ await fetch(entry.removeUrl);
|
|
|
+ } catch { }
|
|
|
+ }
|
|
|
+
|
|
|
render() {
|
|
|
return <div>
|
|
|
- <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control msp-section-header' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button>
|
|
|
-
|
|
|
- <ul style={{ listStyle: 'none' }} className='msp-state-list'>
|
|
|
- {this.state.entries.valueSeq().map(e =><li key={e!.id}>
|
|
|
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.fetch(e!.url)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
|
|
|
- <button onClick={this.remove(e!.removeUrl)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
|
|
|
- <span className='msp-icon msp-icon-remove' />
|
|
|
- </button>
|
|
|
- </li>)}
|
|
|
- </ul>
|
|
|
+ <div className='msp-section-header'>Remote State</div>
|
|
|
+
|
|
|
+ <ParameterControls params={RemoteStateSnapshots.Params} values={this.state.params} onEnter={this.upload} onChange={p => {
|
|
|
+ this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
|
|
|
+ }} isDisabled={this.state.isBusy}/>
|
|
|
+
|
|
|
+ <div className='msp-btn-row-group'>
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isBusy}>Upload</button>
|
|
|
+ <button className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isBusy}>Refresh</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <RemoteStateSnapshotList entries={this.state.entries} isBusy={this.state.isBusy} serverUrl={this.state.params.options.serverUrl}
|
|
|
+ fetch={this.fetch} remove={this.remove} />
|
|
|
</div>;
|
|
|
}
|
|
|
-}
|
|
|
+}
|
|
|
+
|
|
|
+class RemoteStateSnapshotList extends PurePluginUIComponent<
|
|
|
+ { entries: OrderedMap<string, RemoteEntry>, serverUrl: string, isBusy: boolean, fetch: (e: React.MouseEvent<HTMLElement>) => void, remove: (e: React.MouseEvent<HTMLElement>) => void },
|
|
|
+ { }> {
|
|
|
+
|
|
|
+ open = async (e: React.MouseEvent<HTMLElement>) => {
|
|
|
+ const id = e.currentTarget.getAttribute('data-id');
|
|
|
+ if (!id) return;
|
|
|
+ const entry = this.props.entries.get(id);
|
|
|
+ if (!entry) return;
|
|
|
+
|
|
|
+ e.preventDefault();
|
|
|
+ let url = `${window.location}`, qi = url.indexOf('?');
|
|
|
+ if (qi > 0) url = url.substr(0, qi);
|
|
|
+
|
|
|
+ window.open(`${url}?snapshot-url=${encodeURIComponent(entry.url)}`, '_blank');
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
|
|
|
+ {this.props.entries.valueSeq().map(e =><li key={e!.id}>
|
|
|
+ <button data-id={e!.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.props.fetch}
|
|
|
+ disabled={this.props.isBusy} onContextMenu={this.open} title='Click to download, right-click to open in a new tab.'>
|
|
|
+ {e!.name || new Date(e!.timestamp).toLocaleString()} <small>{e!.description}</small>
|
|
|
+ </button>
|
|
|
+ <button data-id={e!.id} onClick={this.props.remove} className='msp-btn msp-btn-link msp-state-list-remove-button' disabled={this.props.isBusy}>
|
|
|
+ <span className='msp-icon msp-icon-remove' />
|
|
|
+ </button>
|
|
|
+ </li>)}
|
|
|
+ </ul>;
|
|
|
+ }
|
|
|
+}
|