@kiberon-labs/behave-graph-flow 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.fallowrc.json +16 -0
- package/.storybook/main.ts +32 -0
- package/.storybook/manager.ts +6 -0
- package/.storybook/preview.ts +64 -0
- package/.storybook/styles.css +16 -0
- package/.turbo/turbo-build.log +7 -0
- package/CHANGELOG.md +368 -0
- package/LICENSE +6 -0
- package/README.md +2 -2
- package/data/Polynomial.json +510 -0
- package/data/sequence.json +337 -0
- package/data/trigger-event.json +241 -0
- package/data/variable-change.json +210 -0
- package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
- package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
- package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
- package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
- package/dist/entry.css +4 -0
- package/dist/index.css +42 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +3597 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18009 -0
- package/dist/index.js.map +1 -0
- package/dist/noteImpl-KkrrWgJd.js +242 -0
- package/dist/noteImpl-KkrrWgJd.js.map +1 -0
- package/dist/styles.module-CvmpDkZj.css +3 -0
- package/dist/styles.module-CvmpDkZj.css.map +1 -0
- package/dist/styles.module-DZxg8aW9.js +271 -0
- package/dist/styles.module-DZxg8aW9.js.map +1 -0
- package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
- package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
- package/docs/notifications.md +246 -0
- package/docs/protocol.md +702 -0
- package/docs/specifics.md +191 -0
- package/package.json +82 -22
- package/postcss.config.ts +3 -4
- package/src/annotations/index.ts +32 -0
- package/src/components/FloatingToolbar/index.module.css +37 -0
- package/src/components/FloatingToolbar/index.tsx +256 -0
- package/src/components/Flow.tsx +287 -75
- package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
- package/src/components/contextMenus/NodePicker.module.css +274 -0
- package/src/components/contextMenus/NodePicker.tsx +481 -0
- package/src/components/contextMenus/edge.tsx +22 -0
- package/src/components/contextMenus/node.tsx +15 -0
- package/src/components/contextMenus/selection.tsx +11 -0
- package/src/components/controls/any/AnyControlImpl.tsx +14 -0
- package/src/components/controls/any/index.tsx +19 -0
- package/src/components/controls/boolean/index.tsx +13 -0
- package/src/components/controls/colorPicker/InputPopover.module.css +100 -0
- package/src/components/controls/colorPicker/InputPopover.tsx +31 -0
- package/src/components/controls/colorPicker/index.module.css +18 -0
- package/src/components/controls/colorPicker/index.tsx +61 -0
- package/src/components/controls/number/index.tsx +35 -0
- package/src/components/controls/string/index.tsx +16 -0
- package/src/components/edges/index.tsx +475 -0
- package/src/components/edges/offsetBezier.ts +134 -0
- package/src/components/hotKeys.tsx +20 -0
- package/src/components/layoutController/index.module.css +13 -0
- package/src/components/layoutController/index.tsx +140 -0
- package/src/components/layoutController/utils.ts +248 -0
- package/src/components/menubar/defaults.tsx +516 -0
- package/src/components/menubar/index.tsx +49 -0
- package/src/components/menubar/menuItem.module.css +31 -0
- package/src/components/menubar/menuItem.tsx +65 -0
- package/src/components/nodes/behave/Node.module.css +23 -0
- package/src/components/nodes/behave/Node.tsx +176 -0
- package/src/components/nodes/behave/NodeContainer.module.css +88 -0
- package/src/components/nodes/behave/NodeContainer.tsx +46 -0
- package/src/components/nodes/behave/index.tsx +14 -0
- package/src/components/nodes/group/index.tsx +109 -0
- package/src/components/nodes/wrapper/index.tsx +73 -0
- package/src/components/nodes/wrapper/styles.module.css +87 -0
- package/src/components/notifications/NotificationProvider.tsx +81 -0
- package/src/components/notifications/index.ts +2 -0
- package/src/components/notifications/utils.ts +71 -0
- package/src/components/panels/alignment/index.module.css +10 -0
- package/src/components/panels/alignment/index.tsx +244 -0
- package/src/components/panels/base/index.tsx +5 -0
- package/src/components/panels/base/styles.module.css +12 -0
- package/src/components/panels/common/PanelHeader.module.css +24 -0
- package/src/components/panels/common/PanelHeader.tsx +22 -0
- package/src/components/panels/common/SectionTitle.module.css +13 -0
- package/src/components/panels/common/SectionTitle.tsx +10 -0
- package/src/components/panels/events/EditEventPanel.tsx +324 -0
- package/src/components/panels/events/ManageEventsPanel.tsx +101 -0
- package/src/components/panels/events/index.tsx +23 -0
- package/src/components/panels/events/styles.module.css +178 -0
- package/src/components/panels/graphProperties/index.tsx +125 -0
- package/src/components/panels/history/index.tsx +92 -0
- package/src/components/panels/history/styles.module.css +97 -0
- package/src/components/panels/keymaps/index.module.css +68 -0
- package/src/components/panels/keymaps/index.tsx +166 -0
- package/src/components/panels/layers/index.tsx +245 -0
- package/src/components/panels/layers/styles.module.css +107 -0
- package/src/components/panels/legend/index.module.css +6 -0
- package/src/components/panels/legend/index.tsx +76 -0
- package/src/components/panels/logs/index.module.css +218 -0
- package/src/components/panels/logs/index.tsx +288 -0
- package/src/components/panels/nodeInputs/InputControl.tsx +63 -0
- package/src/components/panels/nodeInputs/InputsGroup.tsx +65 -0
- package/src/components/panels/nodeInputs/MultipleNodesView.tsx +37 -0
- package/src/components/panels/nodeInputs/NodeSettings.tsx +92 -0
- package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +125 -0
- package/src/components/panels/nodeInputs/OutputsGroup.tsx +55 -0
- package/src/components/panels/nodeInputs/SocketGenerators.tsx +32 -0
- package/src/components/panels/nodeInputs/index.module.css +308 -0
- package/src/components/panels/nodeInputs/index.tsx +349 -0
- package/src/components/panels/nodeInputs/useNodeHandlers.ts +76 -0
- package/src/components/panels/nodeInputs/useNodeInputsData.ts +153 -0
- package/src/components/panels/nodePicker/index.tsx +115 -0
- package/src/components/panels/panel/index.module.css +66 -0
- package/src/components/panels/panel/index.tsx +88 -0
- package/src/components/panels/search/index.module.css +16 -0
- package/src/components/panels/search/index.tsx +215 -0
- package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
- package/src/components/panels/systemSettings/index.tsx +251 -0
- package/src/components/panels/systemSettings/styles.module.css +138 -0
- package/src/components/panels/traces/GridLines.tsx +38 -0
- package/src/components/panels/traces/TimeGrid.tsx +48 -0
- package/src/components/panels/traces/TraceLane.tsx +62 -0
- package/src/components/panels/traces/TraceTooltip.tsx +22 -0
- package/src/components/panels/traces/TracesHeader.tsx +56 -0
- package/src/components/panels/traces/index.module.css +159 -0
- package/src/components/panels/traces/index.tsx +298 -0
- package/src/components/panels/traces/types.ts +48 -0
- package/src/components/panels/traces/useDerivedSpans.ts +307 -0
- package/src/components/panels/traces/utils.ts +33 -0
- package/src/components/panels/variables/CreateVariableScreen.tsx +162 -0
- package/src/components/panels/variables/ManageVariablesScreen.tsx +147 -0
- package/src/components/panels/variables/index.tsx +125 -0
- package/src/components/panels/variables/styles.module.css +149 -0
- package/src/components/primitives/icon.module.css +45 -0
- package/src/components/primitives/icon.tsx +38 -0
- package/src/components/sockets/input/index.tsx +83 -0
- package/src/components/sockets/input/styles.module.css +26 -0
- package/src/components/sockets/output/index.tsx +68 -0
- package/src/components/sockets/output/styles.module.css +22 -0
- package/src/css/notes.css +135 -0
- package/src/css/prosemirror.css +57 -0
- package/src/css/rc-dock.css +212 -0
- package/src/css/rc-menu.css +101 -0
- package/src/css/themes/kiberon.css +127 -0
- package/src/css/vars.css +198 -0
- package/src/css/vscode-elements.css +124 -0
- package/src/entry.css +4 -0
- package/src/generators/CallSubgraphGenerator.tsx +136 -0
- package/src/generators/CustomEventOnTriggeredGenerator.tsx +85 -0
- package/src/generators/GraphBoundaryGenerator.module.css +32 -0
- package/src/generators/GraphBoundaryGenerator.tsx +193 -0
- package/src/generators/SequenceGenerator.tsx +104 -0
- package/src/generators/SwitchOnIntegerGenerator.tsx +256 -0
- package/src/generators/SwitchOnStringGenerator.tsx +263 -0
- package/src/generators/callSubgraphSync.ts +126 -0
- package/src/generators/registerDefaultGenerators.ts +55 -0
- package/src/generators/registerDefaults.ts +26 -0
- package/src/hooks/useBehaveGraphFlow.ts +17 -16
- package/src/hooks/useFlowHandlers.ts +154 -30
- package/src/hooks/useWasdPan.ts +210 -0
- package/src/index.css +134 -0
- package/src/index.ts +53 -18
- package/src/manifest/contributionRegistry.ts +93 -0
- package/src/manifest/index.ts +4 -0
- package/src/manifest/loadManifest.ts +82 -0
- package/src/manifest/manifestPlugin.ts +29 -0
- package/src/manifest/passthroughValueType.ts +40 -0
- package/src/plugin/alignment/index.ts +91 -0
- package/src/plugin/autosave/controller.ts +366 -0
- package/src/plugin/autosave/index.tsx +114 -0
- package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
- package/src/plugin/autosave/panel/index.tsx +1 -0
- package/src/plugin/autosave/panel/styles.module.css +56 -0
- package/src/plugin/autosave/settings.ts +65 -0
- package/src/plugin/autosave/storage.ts +147 -0
- package/src/plugin/docs/index.tsx +297 -0
- package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
- package/src/plugin/docs/panel/index.tsx +21 -0
- package/src/plugin/docs/panel/styles.module.css +174 -0
- package/src/plugin/graphrunner/actions.ts +326 -0
- package/src/plugin/graphrunner/buttons.tsx +95 -0
- package/src/plugin/graphrunner/client.ts +707 -0
- package/src/plugin/graphrunner/index.tsx +184 -0
- package/src/plugin/graphrunner/panel.tsx +386 -0
- package/src/plugin/graphrunner/runController.ts +283 -0
- package/src/plugin/graphrunner/runner.ts +187 -0
- package/src/plugin/graphrunner/session.ts +243 -0
- package/src/plugin/graphrunner/store.ts +196 -0
- package/src/plugin/graphrunner/styles.module.css +171 -0
- package/src/plugin/graphrunner/transport.ts +250 -0
- package/src/plugin/graphrunner/types.ts +693 -0
- package/src/plugin/graphrunner-local/execution-utils.ts +637 -0
- package/src/plugin/graphrunner-local/index.tsx +172 -0
- package/src/plugin/graphrunner-local/panel.tsx +187 -0
- package/src/plugin/graphrunner-local/store.ts +41 -0
- package/src/plugin/graphrunner-local/styles.module.css +82 -0
- package/src/plugin/graphrunner-local/transport.ts +1339 -0
- package/src/plugin/graphrunner-local/types.ts +10 -0
- package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +635 -0
- package/src/plugin/graphrunner-webworker/index.tsx +140 -0
- package/src/plugin/graphrunner-webworker/panel.tsx +173 -0
- package/src/plugin/graphrunner-webworker/store.ts +98 -0
- package/src/plugin/graphrunner-webworker/worker-transport.ts +123 -0
- package/src/plugin/kitchen-sink/index.ts +38 -0
- package/src/plugin/layout/dagre.ts +131 -0
- package/src/plugin/layout/elk.ts +216 -0
- package/src/plugin/layout/index.ts +80 -0
- package/src/plugin/notes/FormatToolbar.tsx +200 -0
- package/src/plugin/notes/index.tsx +191 -0
- package/src/plugin/notes/nodeActions.ts +100 -0
- package/src/plugin/notes/note.tsx +20 -0
- package/src/plugin/notes/noteImpl.tsx +89 -0
- package/src/plugin/realtime/realtimeRunner.ts +624 -0
- package/src/specifics/CustomEventOnTriggeredSpecific.tsx +92 -0
- package/src/specifics/CustomEventTriggerSpecific.tsx +141 -0
- package/src/specifics/VariableGetSpecific.tsx +110 -0
- package/src/specifics/VariableSetSpecific.tsx +110 -0
- package/src/store/actions.tsx +698 -0
- package/src/store/commands.ts +278 -0
- package/src/store/contextMenu.ts +192 -0
- package/src/store/controls.tsx +62 -0
- package/src/store/conversions.ts +47 -0
- package/src/store/documentation.tsx +69 -0
- package/src/store/events.tsx +116 -0
- package/src/store/flow.tsx +230 -0
- package/src/store/graphMeta.ts +39 -0
- package/src/store/hotKeys.tsx +364 -0
- package/src/store/layers.ts +259 -0
- package/src/store/legend.tsx +76 -0
- package/src/store/logs.ts +28 -0
- package/src/store/menubar.ts +41 -0
- package/src/store/refs.ts +84 -0
- package/src/store/registry.ts +51 -0
- package/src/store/selection.ts +22 -0
- package/src/store/settings.ts +99 -0
- package/src/store/settingsSchema.ts +210 -0
- package/src/store/socketGenerator.tsx +54 -0
- package/src/store/specific.tsx +75 -0
- package/src/store/specs.tsx +35 -0
- package/src/store/tabs.ts +282 -0
- package/src/store/toolbar.tsx +45 -0
- package/src/store/traces.ts +240 -0
- package/src/store/variables.ts +37 -0
- package/src/system/graph.ts +131 -0
- package/src/system/graphSession.ts +172 -0
- package/src/system/index.ts +6 -0
- package/src/system/notifications.ts +111 -0
- package/src/system/persistence.ts +82 -0
- package/src/system/plugin.ts +55 -0
- package/src/system/provider.tsx +86 -0
- package/src/system/pubsub.ts +323 -0
- package/src/system/system.ts +653 -0
- package/src/system/tabLoader.tsx +303 -0
- package/src/system/undoRedo.ts +103 -0
- package/src/transformers/Uigraph.ts +61 -0
- package/src/transformers/behaveToFlow.ts +16 -4
- package/src/transformers/contract.ts +87 -0
- package/src/transformers/flowToBehave.ts +40 -12
- package/src/types/NodeMetadata.ts +27 -0
- package/src/types/graph.ts +49 -0
- package/src/types/nodes.ts +50 -0
- package/src/types.ts +18 -0
- package/src/util/autoConvert.ts +200 -0
- package/src/util/colors.ts +1 -29
- package/src/util/downloadJson.ts +18 -0
- package/src/util/extractNodeMetadata.ts +16 -0
- package/src/util/getPickerFilters.ts +1 -1
- package/src/util/isBehaveNode.ts +6 -0
- package/src/util/isValidConnection.ts +51 -17
- package/src/util/mergeSockets.ts +29 -0
- package/src/util/serializeVariables.ts +66 -0
- package/src/util/sockets.ts +43 -0
- package/stories/apex/layoutController/example-graph.worker.ts +39 -0
- package/stories/apex/layoutController/index.stories.tsx +48 -0
- package/stories/apex/layoutController/webworker.stories.tsx +103 -0
- package/stories/apex/menubar/menubar.stories.tsx +19 -0
- package/stories/components/colorpicker/index.stories.tsx +20 -0
- package/stories/components/contextMenus/edge.stories.tsx +32 -0
- package/stories/components/contextMenus/node.stories.tsx +26 -0
- package/stories/components/contextMenus/nodePicker.stories.tsx +115 -0
- package/stories/components/controls/any/index.stories.tsx +19 -0
- package/stories/components/controls/boolean/index.stories.tsx +19 -0
- package/stories/components/controls/colorPicker/index.stories.tsx +49 -0
- package/stories/components/controls/number/index.stories.tsx +19 -0
- package/stories/components/controls/string/index.stories.tsx +19 -0
- package/stories/components/nodes/behaveNode.stories.tsx +108 -0
- package/stories/components/panels/alignment.stories.tsx +24 -0
- package/stories/components/panels/events.stories.tsx +38 -0
- package/stories/components/panels/graphRunner.stories.tsx +317 -0
- package/stories/components/panels/history.stories.tsx +37 -0
- package/stories/components/panels/keymaps.stories.tsx +21 -0
- package/stories/components/panels/legend.stories.tsx +37 -0
- package/stories/components/panels/logs.stories.tsx +24 -0
- package/stories/components/panels/nodeInputs.stories.tsx +21 -0
- package/stories/components/panels/nodePicker.stories.tsx +37 -0
- package/stories/components/panels/panel.stories.tsx +39 -0
- package/stories/components/panels/search.stories.tsx +24 -0
- package/stories/components/panels/systemSettings.stories.tsx +26 -0
- package/stories/components/panels/traces.stories.tsx +225 -0
- package/stories/components/panels/variables.stories.tsx +24 -0
- package/stories/defaults/defaultStoryProvider.tsx +170 -0
- package/stories/defaults/systemGenerator.ts +43 -0
- package/stories/plugins/notes.stories.tsx +100 -0
- package/tests/autoConvert.test.ts +329 -0
- package/tests/autosavePlugin.test.ts +204 -0
- package/tests/callSubgraphSync.test.ts +148 -0
- package/tests/commandRegistry.test.ts +137 -0
- package/tests/components/edges/offsetBezier.test.ts +51 -0
- package/tests/components/layoutController/utils.test.ts +68 -0
- package/tests/components/panels/traces/utils.test.ts +52 -0
- package/tests/contract.test.ts +51 -0
- package/tests/contractSerialize.test.ts +62 -0
- package/tests/deriveSpans.test.ts +71 -0
- package/tests/flowToBehave.test.ts +27 -4
- package/tests/hotkeys.test.ts +79 -0
- package/tests/keepAliveLifecycle.test.ts +167 -0
- package/tests/loadManifest.test.ts +113 -0
- package/tests/noteMarkdown.test.ts +65 -0
- package/tests/notesPlugin.test.ts +162 -0
- package/tests/notifications.test.ts +87 -0
- package/tests/persistence.test.ts +51 -0
- package/tests/saveLoad.test.ts +373 -0
- package/tests/settings.test.ts +178 -0
- package/tests/traceStore.test.ts +46 -0
- package/tests/util/calculateNewEdge.test.ts +98 -0
- package/tests/util/getSocketsByNodeTypeAndHandleType.test.ts +31 -0
- package/tests/util/hasPositionMetaData.test.ts +33 -0
- package/tests/util/isBehaveNode.test.ts +22 -0
- package/tests/util/isHandleConnected.test.ts +37 -0
- package/tests/util/mergeSockets.test.ts +43 -0
- package/tests/visual/README.md +64 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-alignment-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-traces-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
- package/tests/visual/panels.visual.test.tsx +76 -0
- package/tests/wasdPan.test.ts +71 -0
- package/tsconfig.base.json +39 -0
- package/tsconfig.json +18 -59
- package/tsconfig.prod.json +23 -0
- package/tsdown.config.ts +15 -3
- package/typedoc.json +7 -7
- package/vite.config.js +7 -0
- package/vitest.config.ts +5 -2
- package/vitest.visual.config.ts +55 -0
- package/src/components/AutoSizeInput.tsx +0 -65
- package/src/components/Controls.tsx +0 -87
- package/src/components/InputSocket.tsx +0 -142
- package/src/components/Node.tsx +0 -68
- package/src/components/NodeContainer.tsx +0 -46
- package/src/components/NodePicker.tsx +0 -77
- package/src/components/OutputSocket.tsx +0 -58
- package/src/components/modals/ClearModal.tsx +0 -40
- package/src/components/modals/HelpModal.tsx +0 -36
- package/src/components/modals/LoadModal.tsx +0 -96
- package/src/components/modals/Modal.tsx +0 -64
- package/src/components/modals/SaveModal.tsx +0 -60
- package/src/hooks/useCustomNodeTypes.tsx +0 -31
- package/src/hooks/useGraphRunner.ts +0 -104
- package/src/hooks/useMergeMap.ts +0 -14
- package/src/hooks/useNodeSpecJson.ts +0 -20
- package/src/hooks/useQueriableDefinitions.ts +0 -22
- package/src/styles.css +0 -8
- package/tailwind.config.ts +0 -19
- package/tests/tsconfig.json +0 -10
- /package/src/{types.d.ts → types-declarations.d.ts} +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import type { System, SettingsStorage } from '@/system/system';
|
|
4
|
+
import type { GraphSession } from '@/system/graphSession';
|
|
5
|
+
import { buildUIGraphJSON } from '@/transformers/Uigraph';
|
|
6
|
+
import type { UIGraphJSON } from '@/types/graph';
|
|
7
|
+
import { BackupStorage, type BackupSnapshot } from './storage';
|
|
8
|
+
import {
|
|
9
|
+
AUTOSAVE_ENABLED,
|
|
10
|
+
AUTOSAVE_INTERVAL_SECONDS,
|
|
11
|
+
AUTOSAVE_MAX_COPIES,
|
|
12
|
+
AUTOSAVE_DEFAULTS,
|
|
13
|
+
MIN_INTERVAL_SECONDS
|
|
14
|
+
} from './settings';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wait for a graph to be untouched for this long before snapshotting it. A drag
|
|
18
|
+
* or a multi-store edit produces a burst of store updates; requiring a quiet
|
|
19
|
+
* window means we capture the settled result rather than a half-applied frame.
|
|
20
|
+
*/
|
|
21
|
+
const QUIESCE_MS = 800;
|
|
22
|
+
|
|
23
|
+
/** Reactive slice the backup panel subscribes to. */
|
|
24
|
+
export type BackupControllerStore = {
|
|
25
|
+
/** All snapshots across every graph, newest first. */
|
|
26
|
+
snapshots: BackupSnapshot[];
|
|
27
|
+
/** Whether the timer is currently running. */
|
|
28
|
+
running: boolean;
|
|
29
|
+
/** Epoch ms of the last successful capture, or null. */
|
|
30
|
+
lastBackupAt: number | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Per-open-session bookkeeping used to decide when a snapshot is worthwhile. */
|
|
34
|
+
type SessionWatch = {
|
|
35
|
+
/** Changed since the last successful capture. */
|
|
36
|
+
dirty: boolean;
|
|
37
|
+
/** Epoch ms of the most recent store change (for the quiescence window). */
|
|
38
|
+
lastChange: number;
|
|
39
|
+
/** Serialized form of the last captured graph, to skip exact duplicates. */
|
|
40
|
+
lastCapturedJson: string;
|
|
41
|
+
/** Removes the node/edge store subscriptions. */
|
|
42
|
+
unsubscribe: () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every edge must point at nodes that exist. A snapshot taken mid-way through a
|
|
47
|
+
* load or a bulk mutation can have edges referencing not-yet-added (or
|
|
48
|
+
* already-removed) nodes; refusing to persist such a document is the concrete
|
|
49
|
+
* meaning of "do not take a copy during an inconsistent state".
|
|
50
|
+
*/
|
|
51
|
+
const isConsistent = (graph: UIGraphJSON): boolean => {
|
|
52
|
+
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) return false;
|
|
53
|
+
const ids = new Set<string>();
|
|
54
|
+
for (const node of graph.nodes) {
|
|
55
|
+
if (!node || typeof node.id !== 'string') return false;
|
|
56
|
+
ids.add(node.id);
|
|
57
|
+
}
|
|
58
|
+
for (const edge of graph.edges) {
|
|
59
|
+
if (!edge || !edge.source || !edge.target) return false;
|
|
60
|
+
if (!ids.has(edge.source) || !ids.has(edge.target)) return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Drives periodic, consistency-checked backups of every open graph into local
|
|
67
|
+
* storage, and restores from them. Owned by the autosave plugin and exposed on
|
|
68
|
+
* the editor as `system.backups` so panels and other plugins can drive it.
|
|
69
|
+
*
|
|
70
|
+
* Snapshots are only written when a graph has actually changed, has settled
|
|
71
|
+
* (see {@link QUIESCE_MS}), is internally consistent, and is not empty , and
|
|
72
|
+
* never while a caller has the controller suspended. Timer cadence, copy count
|
|
73
|
+
* and the on/off switch are read live from the editor settings.
|
|
74
|
+
*/
|
|
75
|
+
export class BackupController {
|
|
76
|
+
public readonly store: StoreApi<BackupControllerStore>;
|
|
77
|
+
private readonly system: System;
|
|
78
|
+
private readonly backups: BackupStorage;
|
|
79
|
+
|
|
80
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
81
|
+
/** Nesting counter: > 0 means "unsafe to snapshot right now". */
|
|
82
|
+
private suspendDepth = 0;
|
|
83
|
+
private readonly watches = new Map<string, SessionWatch>();
|
|
84
|
+
|
|
85
|
+
private readonly disposers: Array<() => void> = [];
|
|
86
|
+
|
|
87
|
+
constructor(system: System, storage?: SettingsStorage) {
|
|
88
|
+
this.system = system;
|
|
89
|
+
this.backups = new BackupStorage(storage);
|
|
90
|
+
this.store = createStore<BackupControllerStore>(() => ({
|
|
91
|
+
snapshots: this.backups.listAll(),
|
|
92
|
+
running: false,
|
|
93
|
+
lastBackupAt: null
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
// Track sessions as tabs open and close so each open graph is watched.
|
|
97
|
+
this.syncSessions();
|
|
98
|
+
this.disposers.push(
|
|
99
|
+
this.system.activeGraph.subscribe(() => this.syncSessions())
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// React to enable/interval changes without a restart.
|
|
103
|
+
this.disposers.push(
|
|
104
|
+
this.system.systemSettings.subscribe(() => this.applySettings())
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
this.applySettings();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Suspension , the "no copy during inconsistent state" API.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
/** Mark the start of a region where a snapshot would be unsafe. */
|
|
115
|
+
suspend(): void {
|
|
116
|
+
this.suspendDepth += 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** End a region opened by {@link suspend}. */
|
|
120
|
+
resume(): void {
|
|
121
|
+
if (this.suspendDepth > 0) this.suspendDepth -= 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether snapshots are currently suspended. */
|
|
125
|
+
get suspended(): boolean {
|
|
126
|
+
return this.suspendDepth > 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Run `fn` with snapshots suspended, resuming even if it throws. Use this to
|
|
131
|
+
* bracket bulk mutations (loads, imports, programmatic rewrites) so an
|
|
132
|
+
* autosave tick can never capture the graph mid-transition.
|
|
133
|
+
*/
|
|
134
|
+
runExclusive<T>(fn: () => T): T {
|
|
135
|
+
this.suspend();
|
|
136
|
+
try {
|
|
137
|
+
return fn();
|
|
138
|
+
} finally {
|
|
139
|
+
this.resume();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Scheduling.
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
private intervalMs(): number {
|
|
148
|
+
const raw = Number(
|
|
149
|
+
this.system.getSetting(AUTOSAVE_INTERVAL_SECONDS) ??
|
|
150
|
+
AUTOSAVE_DEFAULTS.intervalSeconds
|
|
151
|
+
);
|
|
152
|
+
const seconds = Number.isFinite(raw)
|
|
153
|
+
? Math.max(MIN_INTERVAL_SECONDS, raw)
|
|
154
|
+
: AUTOSAVE_DEFAULTS.intervalSeconds;
|
|
155
|
+
return seconds * 1000;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private maxCopies(): number {
|
|
159
|
+
const raw = Number(
|
|
160
|
+
this.system.getSetting(AUTOSAVE_MAX_COPIES) ?? AUTOSAVE_DEFAULTS.maxCopies
|
|
161
|
+
);
|
|
162
|
+
return Number.isFinite(raw) && raw >= 1
|
|
163
|
+
? Math.floor(raw)
|
|
164
|
+
: AUTOSAVE_DEFAULTS.maxCopies;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** (Re)start or stop the timer to match the current settings. */
|
|
168
|
+
private applySettings(): void {
|
|
169
|
+
const enabled =
|
|
170
|
+
this.system.getSetting<boolean>(AUTOSAVE_ENABLED) === true &&
|
|
171
|
+
this.backups.available;
|
|
172
|
+
if (enabled) this.start();
|
|
173
|
+
else this.stop();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private start(): void {
|
|
177
|
+
this.stop();
|
|
178
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs());
|
|
179
|
+
this.store.setState({ running: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private stop(): void {
|
|
183
|
+
if (this.timer) {
|
|
184
|
+
clearInterval(this.timer);
|
|
185
|
+
this.timer = undefined;
|
|
186
|
+
}
|
|
187
|
+
this.store.setState({ running: false });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** One scheduler pass: try to capture each open, dirty, settled graph. */
|
|
191
|
+
private tick(): void {
|
|
192
|
+
if (this.suspended) return;
|
|
193
|
+
for (const session of Object.values(
|
|
194
|
+
this.system.activeGraph.getState().sessions
|
|
195
|
+
)) {
|
|
196
|
+
this.captureSession(session);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Capture.
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Snapshot one graph now if it is worth it: changed, settled, non-empty,
|
|
206
|
+
* consistent and not a duplicate of the last capture. Returns the snapshot on
|
|
207
|
+
* success, or null when it was skipped.
|
|
208
|
+
*/
|
|
209
|
+
captureSession(session: GraphSession, force = false): BackupSnapshot | null {
|
|
210
|
+
if (this.suspended) return null;
|
|
211
|
+
const watch = this.watches.get(session.id);
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
|
|
214
|
+
if (!force) {
|
|
215
|
+
if (!watch?.dirty) return null;
|
|
216
|
+
if (now - watch.lastChange < QUIESCE_MS) return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let graph: UIGraphJSON;
|
|
220
|
+
try {
|
|
221
|
+
graph = buildUIGraphJSON(session);
|
|
222
|
+
} catch {
|
|
223
|
+
// A graph that can't even be serialized is by definition inconsistent.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Skip empty graphs: an accidental "Clear" should not evict good backups.
|
|
228
|
+
if (graph.nodes.length === 0) {
|
|
229
|
+
if (watch) watch.dirty = false;
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!isConsistent(graph)) return null;
|
|
234
|
+
|
|
235
|
+
const json = JSON.stringify(graph);
|
|
236
|
+
if (watch && json === watch.lastCapturedJson) {
|
|
237
|
+
watch.dirty = false;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const snapshot: BackupSnapshot = {
|
|
242
|
+
id: uuidv4(),
|
|
243
|
+
graphId: session.id,
|
|
244
|
+
name: session.name,
|
|
245
|
+
timestamp: now,
|
|
246
|
+
nodeCount: graph.nodes.length,
|
|
247
|
+
graph
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
this.backups.append(snapshot, this.maxCopies());
|
|
251
|
+
if (watch) {
|
|
252
|
+
watch.dirty = false;
|
|
253
|
+
watch.lastCapturedJson = json;
|
|
254
|
+
}
|
|
255
|
+
this.store.setState({
|
|
256
|
+
snapshots: this.backups.listAll(),
|
|
257
|
+
lastBackupAt: now
|
|
258
|
+
});
|
|
259
|
+
return snapshot;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Force an immediate backup of the focused graph (the "Back up now" action). */
|
|
263
|
+
backupNow(): BackupSnapshot | null {
|
|
264
|
+
const session = this.system.session;
|
|
265
|
+
if (!session) return null;
|
|
266
|
+
return this.captureSession(session, true);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Restore + management.
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Restore a snapshot into a brand-new graph tab, leaving all currently open
|
|
275
|
+
* graphs untouched , the safe choice when recovering from a bad state.
|
|
276
|
+
* Returns the new session, or undefined if the snapshot is gone.
|
|
277
|
+
*/
|
|
278
|
+
restore(snapshotId: string): GraphSession | undefined {
|
|
279
|
+
const snapshot = this.backups.find(snapshotId);
|
|
280
|
+
if (!snapshot) return undefined;
|
|
281
|
+
|
|
282
|
+
const session = this.system.newGraph(`${snapshot.name} (restored)`);
|
|
283
|
+
this.runExclusive(() => {
|
|
284
|
+
session.graph.deseralize(snapshot.graph);
|
|
285
|
+
session.flowStore
|
|
286
|
+
.getState()
|
|
287
|
+
.setGraph(snapshot.graph.flow, { skipLayout: true });
|
|
288
|
+
});
|
|
289
|
+
this.system.notifications.success(
|
|
290
|
+
`Restored "${snapshot.name}" into a new tab`
|
|
291
|
+
);
|
|
292
|
+
return session;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Delete one snapshot. */
|
|
296
|
+
deleteSnapshot(id: string): void {
|
|
297
|
+
this.backups.remove(id);
|
|
298
|
+
this.refresh();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Delete every snapshot. */
|
|
302
|
+
clearAll(): void {
|
|
303
|
+
this.backups.clear();
|
|
304
|
+
this.refresh();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Re-read storage into the reactive store (after an external change). */
|
|
308
|
+
refresh(): void {
|
|
309
|
+
this.store.setState({ snapshots: this.backups.listAll() });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Session watching.
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/** Add watches for newly opened sessions, drop them for closed ones. */
|
|
317
|
+
private syncSessions(): void {
|
|
318
|
+
const sessions = this.system.activeGraph.getState().sessions;
|
|
319
|
+
// Add watches for new sessions.
|
|
320
|
+
for (const session of Object.values(sessions)) {
|
|
321
|
+
if (!this.watches.has(session.id)) this.watchSession(session);
|
|
322
|
+
}
|
|
323
|
+
// Remove watches for sessions that have gone away.
|
|
324
|
+
for (const [id, watch] of this.watches) {
|
|
325
|
+
if (!(id in sessions)) {
|
|
326
|
+
watch.unsubscribe();
|
|
327
|
+
this.watches.delete(id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private watchSession(session: GraphSession): void {
|
|
333
|
+
const markDirty = () => {
|
|
334
|
+
const watch = this.watches.get(session.id);
|
|
335
|
+
if (!watch) return;
|
|
336
|
+
watch.dirty = true;
|
|
337
|
+
watch.lastChange = Date.now();
|
|
338
|
+
};
|
|
339
|
+
const unsubNodes = session.nodeStore.subscribe(markDirty);
|
|
340
|
+
const unsubEdges = session.edgeStore.subscribe(markDirty);
|
|
341
|
+
this.watches.set(session.id, {
|
|
342
|
+
dirty: false,
|
|
343
|
+
lastChange: 0,
|
|
344
|
+
lastCapturedJson: '',
|
|
345
|
+
unsubscribe: () => {
|
|
346
|
+
unsubNodes();
|
|
347
|
+
unsubEdges();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Tear down timers and subscriptions. */
|
|
353
|
+
dispose(): void {
|
|
354
|
+
this.stop();
|
|
355
|
+
for (const watch of this.watches.values()) watch.unsubscribe();
|
|
356
|
+
this.watches.clear();
|
|
357
|
+
for (const dispose of this.disposers) {
|
|
358
|
+
try {
|
|
359
|
+
dispose();
|
|
360
|
+
} catch {
|
|
361
|
+
// ignore disposer errors
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this.disposers.length = 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
2
|
+
import { plugin } from '@/system/plugin';
|
|
3
|
+
import type { System, SettingsStorage } from '@/system/system';
|
|
4
|
+
import { MenuItemElement } from '@/components/menubar/menuItem';
|
|
5
|
+
import { BackupController } from './controller';
|
|
6
|
+
import { BackupPanel } from './panel';
|
|
7
|
+
import { AUTOSAVE_SETTINGS } from './settings';
|
|
8
|
+
|
|
9
|
+
export * from './settings';
|
|
10
|
+
export * from './storage';
|
|
11
|
+
export * from './controller';
|
|
12
|
+
|
|
13
|
+
declare module '@/system/system' {
|
|
14
|
+
interface System {
|
|
15
|
+
/** Local backup controller, present when the autosave plugin is loaded. */
|
|
16
|
+
backups?: BackupController;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Tab id for the backup browser panel. */
|
|
21
|
+
export const BACKUP_PANEL_TAB_ID = 'autosaveBackups';
|
|
22
|
+
|
|
23
|
+
/** Options for {@link autosavePlugin}. */
|
|
24
|
+
export interface AutosavePluginOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Storage adapter for the backups. Defaults to `localStorage`. Provide your
|
|
27
|
+
* own (e.g. backed by VS Code workspace state) to persist elsewhere; pass a
|
|
28
|
+
* throwaway map in tests.
|
|
29
|
+
*/
|
|
30
|
+
storage?: SettingsStorage;
|
|
31
|
+
/** Add the "Local Backups" item to the Window menu. Default: true. */
|
|
32
|
+
addMenuItem?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Client-side automatic graph backups.
|
|
37
|
+
*
|
|
38
|
+
* Periodically snapshots every open graph into local storage so a crash or a
|
|
39
|
+
* bad edit during development is recoverable, and adds a panel to browse and
|
|
40
|
+
* restore those copies. Everything stays in the browser , no backend.
|
|
41
|
+
*
|
|
42
|
+
* The plugin:
|
|
43
|
+
* - contributes the `Autosave` settings (on/off, frequency, copies-to-keep);
|
|
44
|
+
* - installs a {@link BackupController} on the editor as `system.backups`,
|
|
45
|
+
* which runs the consistency-checked capture loop and honours the settings;
|
|
46
|
+
* - registers the `autosaveBackups` panel plus `autosave.openBackups` /
|
|
47
|
+
* `autosave.backupNow` commands and a Window-menu entry.
|
|
48
|
+
*
|
|
49
|
+
* Snapshots are only taken when a graph changed, settled, is consistent and is
|
|
50
|
+
* not empty; callers can bracket bulk mutations with
|
|
51
|
+
* `system.backups.runExclusive(...)` to guarantee no copy is taken mid-change.
|
|
52
|
+
*/
|
|
53
|
+
export const autosavePlugin = plugin(
|
|
54
|
+
(system: System, options: AutosavePluginOptions = {}) => {
|
|
55
|
+
system.registerSettings(AUTOSAVE_SETTINGS);
|
|
56
|
+
|
|
57
|
+
const controller = new BackupController(system, options.storage);
|
|
58
|
+
system.decorate('backups', controller);
|
|
59
|
+
|
|
60
|
+
const commands = system.commandStore.getState();
|
|
61
|
+
commands.register({
|
|
62
|
+
id: 'autosave.openBackups',
|
|
63
|
+
title: 'Open Local Backups',
|
|
64
|
+
run: (ctx) => ctx.editor.tabStore.getState().openTab(BACKUP_PANEL_TAB_ID)
|
|
65
|
+
});
|
|
66
|
+
commands.register({
|
|
67
|
+
id: 'autosave.backupNow',
|
|
68
|
+
title: 'Back Up Graph Now',
|
|
69
|
+
run: (ctx) => {
|
|
70
|
+
ctx.editor.backups?.backupNow();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
system.tabLoader.register(BACKUP_PANEL_TAB_ID, () => ({
|
|
75
|
+
id: BACKUP_PANEL_TAB_ID,
|
|
76
|
+
closable: true,
|
|
77
|
+
title: 'Local Backups',
|
|
78
|
+
group: 'default',
|
|
79
|
+
content: () => (
|
|
80
|
+
<ErrorBoundary fallback={'Error loading Local Backups panel'}>
|
|
81
|
+
<BackupPanel />
|
|
82
|
+
</ErrorBoundary>
|
|
83
|
+
)
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
if (options.addMenuItem !== false) {
|
|
87
|
+
const menuStore = system.menubarStore;
|
|
88
|
+
const windowMenu = menuStore
|
|
89
|
+
.getState()
|
|
90
|
+
.items.find((menu) => menu.name === 'window');
|
|
91
|
+
if (windowMenu) {
|
|
92
|
+
menuStore.getState().setSubMenuItems('window', [
|
|
93
|
+
...windowMenu.items,
|
|
94
|
+
{
|
|
95
|
+
name: 'autosaveBackups',
|
|
96
|
+
render: function BackupsMenuItem() {
|
|
97
|
+
return (
|
|
98
|
+
<MenuItemElement
|
|
99
|
+
key="autosaveBackups"
|
|
100
|
+
onClick={() =>
|
|
101
|
+
system.tabStore.getState().openTab(BACKUP_PANEL_TAB_ID)
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
Local Backups
|
|
105
|
+
</MenuItemElement>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{ name: 'autosave' }
|
|
114
|
+
);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useStore } from 'zustand';
|
|
2
|
+
import {
|
|
3
|
+
VscodeButton,
|
|
4
|
+
VscodeLabel,
|
|
5
|
+
VscodeTable,
|
|
6
|
+
VscodeTableBody,
|
|
7
|
+
VscodeTableCell,
|
|
8
|
+
VscodeTableHeader,
|
|
9
|
+
VscodeTableHeaderCell,
|
|
10
|
+
VscodeTableRow
|
|
11
|
+
} from '@vscode-elements/react-elements';
|
|
12
|
+
import { useSystem } from '@/system/provider';
|
|
13
|
+
import { BasePanel } from '@/components/panels/base';
|
|
14
|
+
import type { BackupController } from '../controller';
|
|
15
|
+
import type { BackupSnapshot } from '../storage';
|
|
16
|
+
import styles from './styles.module.css';
|
|
17
|
+
|
|
18
|
+
const formatTime = (ms: number): string => {
|
|
19
|
+
try {
|
|
20
|
+
return new Date(ms).toLocaleString();
|
|
21
|
+
} catch {
|
|
22
|
+
return String(ms);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Browse and restore local graph backups. Restoring opens the snapshot in a new
|
|
28
|
+
* tab so the current work is never overwritten. Rendered from the autosave
|
|
29
|
+
* plugin's registered `autosaveBackups` tab.
|
|
30
|
+
*/
|
|
31
|
+
export const BackupPanel = () => {
|
|
32
|
+
const system = useSystem();
|
|
33
|
+
const controller = system.backups as BackupController | undefined;
|
|
34
|
+
|
|
35
|
+
if (!controller) {
|
|
36
|
+
return (
|
|
37
|
+
<BasePanel>
|
|
38
|
+
<div className={styles.empty}>Backups are not available.</div>
|
|
39
|
+
</BasePanel>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return <BackupPanelBody controller={controller} />;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const BackupPanelBody = ({ controller }: { controller: BackupController }) => {
|
|
47
|
+
const snapshots = useStore(controller.store, (s) => s.snapshots);
|
|
48
|
+
const running = useStore(controller.store, (s) => s.running);
|
|
49
|
+
const lastBackupAt = useStore(controller.store, (s) => s.lastBackupAt);
|
|
50
|
+
|
|
51
|
+
const onRestore = (snapshot: BackupSnapshot) => {
|
|
52
|
+
controller.restore(snapshot.id);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onDelete = (snapshot: BackupSnapshot) => {
|
|
56
|
+
controller.deleteSnapshot(snapshot.id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onClearAll = () => {
|
|
60
|
+
if (
|
|
61
|
+
typeof confirm === 'function' &&
|
|
62
|
+
!confirm('Delete all local backups? This cannot be undone.')
|
|
63
|
+
) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
controller.clearAll();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<BasePanel>
|
|
71
|
+
<div className={styles.content}>
|
|
72
|
+
<div className={styles.header}>
|
|
73
|
+
<VscodeLabel>Local Backups</VscodeLabel>
|
|
74
|
+
<span className={styles.helpText}>
|
|
75
|
+
{running
|
|
76
|
+
? 'Autosave is on. Snapshots are kept in this browser only.'
|
|
77
|
+
: 'Autosave is off. Enable it in Settings, or back up on demand.'}
|
|
78
|
+
{lastBackupAt ? ` Last backup: ${formatTime(lastBackupAt)}.` : ''}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className={styles.toolbar}>
|
|
83
|
+
<VscodeButton onClick={() => controller.backupNow()}>
|
|
84
|
+
Back up now
|
|
85
|
+
</VscodeButton>
|
|
86
|
+
<VscodeButton
|
|
87
|
+
secondary
|
|
88
|
+
disabled={snapshots.length === 0}
|
|
89
|
+
onClick={onClearAll}
|
|
90
|
+
>
|
|
91
|
+
Clear all
|
|
92
|
+
</VscodeButton>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{snapshots.length === 0 ? (
|
|
96
|
+
<div className={styles.empty}>
|
|
97
|
+
No backups yet. A copy is saved automatically when a graph changes
|
|
98
|
+
(with autosave on), or press “Back up now”.
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<VscodeTable className={styles.table} zebra>
|
|
102
|
+
<VscodeTableHeader slot="header">
|
|
103
|
+
<VscodeTableHeaderCell>Graph</VscodeTableHeaderCell>
|
|
104
|
+
<VscodeTableHeaderCell>When</VscodeTableHeaderCell>
|
|
105
|
+
<VscodeTableHeaderCell>Nodes</VscodeTableHeaderCell>
|
|
106
|
+
<VscodeTableHeaderCell>Actions</VscodeTableHeaderCell>
|
|
107
|
+
</VscodeTableHeader>
|
|
108
|
+
<VscodeTableBody slot="body">
|
|
109
|
+
{snapshots.map((snapshot) => (
|
|
110
|
+
<VscodeTableRow key={snapshot.id}>
|
|
111
|
+
<VscodeTableCell className={styles.nameCell}>
|
|
112
|
+
<span className={styles.name} title={snapshot.name}>
|
|
113
|
+
{snapshot.name || 'Untitled'}
|
|
114
|
+
</span>
|
|
115
|
+
</VscodeTableCell>
|
|
116
|
+
<VscodeTableCell>
|
|
117
|
+
{formatTime(snapshot.timestamp)}
|
|
118
|
+
</VscodeTableCell>
|
|
119
|
+
<VscodeTableCell>{snapshot.nodeCount}</VscodeTableCell>
|
|
120
|
+
<VscodeTableCell className={styles.actionsCell}>
|
|
121
|
+
<div className={styles.buttonGroup}>
|
|
122
|
+
<VscodeButton onClick={() => onRestore(snapshot)}>
|
|
123
|
+
Restore
|
|
124
|
+
</VscodeButton>
|
|
125
|
+
<VscodeButton
|
|
126
|
+
secondary
|
|
127
|
+
onClick={() => onDelete(snapshot)}
|
|
128
|
+
>
|
|
129
|
+
Delete
|
|
130
|
+
</VscodeButton>
|
|
131
|
+
</div>
|
|
132
|
+
</VscodeTableCell>
|
|
133
|
+
</VscodeTableRow>
|
|
134
|
+
))}
|
|
135
|
+
</VscodeTableBody>
|
|
136
|
+
</VscodeTable>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</BasePanel>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BackupPanel } from './BackupPanel';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
.content {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 0.25rem;
|
|
11
|
+
padding-bottom: 0.25rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.helpText {
|
|
15
|
+
font-size: 0.75rem;
|
|
16
|
+
opacity: 0.7;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.toolbar {
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.table {
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.nameCell {
|
|
29
|
+
width: 40%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.name {
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
display: inline-block;
|
|
38
|
+
max-width: 100%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.actionsCell {
|
|
42
|
+
text-align: right;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.buttonGroup {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 0.25rem;
|
|
48
|
+
justify-content: flex-end;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.empty {
|
|
53
|
+
font-size: 0.8125rem;
|
|
54
|
+
opacity: 0.7;
|
|
55
|
+
padding: 1rem 0;
|
|
56
|
+
}
|