/** * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal * @author Alexander Rose */ import { OrderedMap } from 'immutable'; import * as React from 'react'; import { PluginCommands } from '../../mol-plugin/commands'; import { PluginConfig } from '../../mol-plugin/config'; import { PluginState } from '../../mol-plugin/state'; import { shallowEqualObjects } from '../../mol-util'; import { formatTimespan } from '../../mol-util/now'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { urlCombine } from '../../mol-util/url'; import { PluginUIComponent, PurePluginUIComponent } from '../base'; import { Button, ExpandGroup, IconButton, SectionHeader } from '../controls/common'; import { Icon, SaveOutlinedSvg, GetAppSvg, OpenInBrowserSvg, WarningSvg, DeleteOutlinedSvg, AddSvg, ArrowUpwardSvg, SwapHorizSvg, ArrowDownwardSvg, RefreshSvg, CloudUploadSvg } from '../controls/icons'; import { ParameterControls } from '../controls/parameters'; export class StateSnapshots extends PluginUIComponent<{}> { render() { return
{this.plugin.spec.components?.remoteState !== 'none' && }
; } } export class StateExportImportControls extends PluginUIComponent<{ onAction?: () => void }> { downloadToFileJson = () => { this.props.onAction?.(); PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'json' }); }; downloadToFileZip = () => { this.props.onAction?.(); PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'zip' }); }; open = (e: React.ChangeEvent) => { if (!e.target.files || !e.target.files[0]) { this.plugin.log.error('No state file selected'); return; } this.props.onAction?.(); PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: e.target.files[0] }); }; render() { return <>
Open
This is an experimental feature and stored states/sessions might not be openable in a future version.
; } } export class LocalStateSnapshotParams extends PluginUIComponent { componentDidMount() { this.subscribe(this.plugin.state.snapshotParams, () => this.forceUpdate()); } render() { return ; } } export class LocalStateSnapshots extends PluginUIComponent< {}, { params: PD.Values }> { state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) }; static Params = { name: PD.Text(), description: PD.Text() }; add = () => { PluginCommands.State.Snapshots.Add(this.plugin, { name: this.state.params.name, description: this.state.params.description }); }; updateParams = (params: PD.Values) => this.setState({ params }); clear = () => { PluginCommands.State.Snapshots.Clear(this.plugin, {}); }; shouldComponentUpdate(nextProps: any, nextState: any) { return !shallowEqualObjects(this.props, nextProps) || !shallowEqualObjects(this.state, nextState); } render() { return
; } } export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> { componentDidMount() { this.subscribe(this.plugin.managers.snapshot.events.changed, () => this.forceUpdate()); } apply = (e: React.MouseEvent) => { const id = e.currentTarget.getAttribute('data-id'); if (!id) return; PluginCommands.State.Snapshots.Apply(this.plugin, { id }); }; remove = (e: React.MouseEvent) => { const id = e.currentTarget.getAttribute('data-id'); if (!id) return; PluginCommands.State.Snapshots.Remove(this.plugin, { id }); }; moveUp = (e: React.MouseEvent) => { const id = e.currentTarget.getAttribute('data-id'); if (!id) return; PluginCommands.State.Snapshots.Move(this.plugin, { id, dir: -1 }); }; moveDown = (e: React.MouseEvent) => { const id = e.currentTarget.getAttribute('data-id'); if (!id) return; PluginCommands.State.Snapshots.Move(this.plugin, { id, dir: 1 }); }; replace = (e: React.MouseEvent) => { // TODO: add option change name/description const id = e.currentTarget.getAttribute('data-id'); if (!id) return; PluginCommands.State.Snapshots.Replace(this.plugin, { id }); }; render() { const current = this.plugin.managers.snapshot.state.current; const items: JSX.Element[] = []; this.plugin.managers.snapshot.state.entries.forEach(e => { items.push(
  • ); const image = e.image && this.plugin.managers.asset.get(e.image)?.file; if (image) { items.push(
  • ); } }); return <>
      {items}
    ; } } export type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string, isSticky?: boolean } export class RemoteStateSnapshots extends PluginUIComponent< { listOnly?: boolean }, { params: PD.Values, entries: OrderedMap, isBusy: boolean }> { Params = { name: PD.Text(), options: PD.Group({ description: PD.Text(), playOnLoad: PD.Boolean(false), serverUrl: PD.Text(this.plugin.config.get(PluginConfig.State.CurrentServer)) }) }; state = { params: PD.getDefaultValues(this.Params), entries: OrderedMap(), isBusy: false }; ListOnlyParams = { options: PD.Group({ serverUrl: PD.Text(this.plugin.config.get(PluginConfig.State.CurrentServer)) }, { isFlat: true }) }; private _mounted = false; componentDidMount() { this.refresh(); // TODO: solve this by using "PluginComponent" with behaviors intead this._mounted = true; // this.subscribe(UploadedEvent, this.refresh); } componentWillUnmount() { super.componentWillUnmount(); this._mounted = false; } serverUrl(q?: string) { if (!q) return this.state.params.options.serverUrl; return urlCombine(this.state.params.options.serverUrl, q); } refresh = async () => { try { this.setState({ isBusy: true }); this.plugin.config.set(PluginConfig.State.CurrentServer, this.state.params.options.serverUrl); const json = (await this.plugin.runTask(this.plugin.fetch({ url: this.serverUrl('list'), type: 'json' }))) || []; json.sort((a, b) => { if (a.isSticky === b.isSticky) return a.timestamp - b.timestamp; return a.isSticky ? -1 : 1; }); const entries = OrderedMap().asMutable(); for (const e of json) { entries.set(e.id, { ...e, url: this.serverUrl(`get/${e.id}`), removeUrl: this.serverUrl(`remove/${e.id}`) }); } if (this._mounted) this.setState({ entries: entries.asImmutable(), isBusy: false }); } catch (e) { console.error(e); this.plugin.log.error('Error fetching remote snapshots'); if (this._mounted) this.setState({ entries: OrderedMap(), isBusy: false }); } }; upload = async () => { this.setState({ isBusy: true }); this.plugin.config.set(PluginConfig.State.CurrentServer, this.state.params.options.serverUrl); await PluginCommands.State.Snapshots.Upload(this.plugin, { name: this.state.params.name, description: this.state.params.options.description, playOnLoad: this.state.params.options.playOnLoad, serverUrl: this.state.params.options.serverUrl }); this.plugin.log.message('Snapshot uploaded.'); if (this._mounted) { this.setState({ isBusy: false }); this.refresh(); } }; fetch = async (e: React.MouseEvent) => { 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(this.plugin, { url: entry.url }); } finally { if (this._mounted) this.setState({ isBusy: false }); } }; remove = async (e: React.MouseEvent) => { 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 (e) { console.error(e); } }; render() { return <> {!this.props.listOnly && <> { this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any); }} isDisabled={this.state.isBusy} />
    } {this.props.listOnly &&
    { this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any); }} isDisabled={this.state.isBusy} />
    } ; } } class RemoteStateSnapshotList extends PurePluginUIComponent< { entries: OrderedMap, serverUrl: string, isBusy: boolean, fetch: (e: React.MouseEvent) => void, remove?: (e: React.MouseEvent) => void }, {}> { open = async (e: React.MouseEvent) => { 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}`; const qi = url.indexOf('?'); if (qi > 0) url = url.substr(0, qi); window.open(`${url}?snapshot-url=${encodeURIComponent(entry.url)}`, '_blank'); }; render() { return
      {this.props.entries.valueSeq().map(e =>
    • {!e!.isSticky && this.props.remove && }
    • )}
    ; } }