single_model.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import * as React from "react";
  2. import * as Mantine from "@mantine/core";
  3. import * as Icons from "@tabler/icons";
  4. import {D3OnionGraphData, D3GraphUpdater} from "../d3graph/reducers/onion_graph";
  5. import {D3GraphEditable, UserEditCallback} from "../d3graph/d3graph_editable";
  6. import {
  7. DeltaGraphState,
  8. fullDeltaId,
  9. deltaGraphReducer,
  10. } from "../d3graph/reducers/delta_graph";
  11. import {
  12. HistoryGraphState,
  13. initialHistoryGraph,
  14. fullVersionId,
  15. historyGraphReducer,
  16. } from "../d3graph/reducers/history_graph";
  17. import * as helpText from "./help_text";
  18. import {MergeView} from "./merge_view";
  19. import {GraphView, Renderer} from "./graph_view";
  20. import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
  21. import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
  22. import {InfoHoverCardOverlay} from "../info_hover_card";
  23. import {OnionContext, OnionContextType} from "../onion_context";
  24. import {Version, VersionRegistry, Embeddings} from "onion/version";
  25. import {PrimitiveDelta, PrimitiveRegistry} from "onion/primitive_delta";
  26. import {PrimitiveValue, UUID} from "onion/types";
  27. import {CompositeDelta, CompositeLevel} from "onion/composite_delta";
  28. import {GraphState} from "onion/graph_state";
  29. import {Delta} from "onion/delta";
  30. import {DeltaParser} from "onion/delta_parser";
  31. export const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
  32. export interface VersionedModelState {
  33. version: Version; // the 'current version'
  34. graph: D3OnionGraphData; // the state what is displayed in the leftmost panel
  35. historyGraph: HistoryGraphState; // the state of what is displayed in the middle panel
  36. deltaGraphL1: DeltaGraphState; // the state of what is displayed in the rightmost panel
  37. deltaGraphL0: DeltaGraphState; // the state of what is displayed in the rightmost panel
  38. }
  39. interface VersionedModelCallbacks {
  40. onUserEdit?: UserEditCallback;
  41. onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void;
  42. onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void;
  43. onVersionClicked?: (Version) => void;
  44. onMerge?: (outputs: Version[]) => void;
  45. }
  46. // Basically everything we need to construct the React components for:
  47. // - Graph state (+ optionally, a Rountangle Editor)
  48. // - History graph (+undo/redo buttons)
  49. // - Delta graph
  50. // , their state, and callbacks for updating their state.
  51. export function newOnion({readonly, primitiveRegistry, versionRegistry}) {
  52. const graphState = new GraphState();
  53. const compositeLevel = new CompositeLevel();
  54. // SVG coordinates to be used when adding a new node
  55. let x = 0;
  56. let y = 0;
  57. // The "current version" is both part of the React state (for rendering undo/redo buttons) and a local variable here, such that we can get the current version (synchronously), even outside of a setState-callback.
  58. let currentVersion = versionRegistry.initialVersion;
  59. function getCurrentVersion() {
  60. return currentVersion;
  61. }
  62. function useOnion(overridenCallbacks: (any) => VersionedModelCallbacks) {
  63. const [version, setVersion] = React.useState<Version>(versionRegistry.initialVersion);
  64. const [graph, setGraph] = React.useState<D3OnionGraphData>(emptyGraph);
  65. const [historyGraph, setHistoryGraph] = React.useState<HistoryGraphState>(initialHistoryGraph(versionRegistry.initialVersion));
  66. const [deltaGraphL1, setDeltaGraphL1] = React.useState<DeltaGraphState>(emptyGraph);
  67. const [deltaGraphL0, setDeltaGraphL0] = React.useState<DeltaGraphState>(emptyGraph);
  68. // Reducer
  69. // Create and add a new version, and its deltas, without changing the current version
  70. const createVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer, embeddings: (Version) => Embeddings = () => new Map()) => {
  71. const parentVersion = versionRegistry.lookupOptional(parentHash);
  72. if (parentVersion !== undefined) {
  73. // The following is not very efficient, and it looks weird and hacky, but it works.
  74. // Cleaner looking solution would be to implement a function version.getGraphState() ...
  75. let prevVersion = currentVersion;
  76. gotoVersion(parentVersion);
  77. const dependencies = compositeLevel.findCompositeDependencies(deltas, graphState.composites)
  78. const composite = compositeLevel.createComposite(deltas, description, dependencies);
  79. gotoVersion(prevVersion); // go back
  80. const newVersion = versionRegistry.createVersion(parentVersion, composite, embeddings);
  81. setHistoryGraph(historyGraph => historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}));
  82. setDeltaGraphL1(deltaGraphL1 => composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1);
  83. setDeltaGraphL0(deltaGraphL0 => composite.deltas.reduce((graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}), deltaGraphL0));
  84. return newVersion;
  85. }
  86. };
  87. const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion, embeddings: (Version) => Embeddings = () => new Map()): Version => {
  88. const newVersion = createVersion(deltas, description, parentVersion.hash, embeddings) as Version;
  89. gotoVersion(newVersion);
  90. return newVersion;
  91. };
  92. // Idempotent
  93. const appendVersions = (versions: Version[]) => {
  94. setHistoryGraph(historyGraph => {
  95. const versionsToAdd: Version[] = [];
  96. const addIfDontHaveYet = version => {
  97. if (!(historyGraph.nodes.some(n => n.obj === version) || versionsToAdd.includes(version))) {
  98. collectVersions(version);
  99. }
  100. }
  101. const collectVersions = version => {
  102. // first add child, then parent
  103. // this prevents infinite recursion when a version explicitly embeds itself.
  104. versionsToAdd.push(version);
  105. for (const [parent] of version.parents) {
  106. addIfDontHaveYet(parent);
  107. }
  108. for (const {version: guest} of version.embeddings.values()) {
  109. addIfDontHaveYet(guest);
  110. }
  111. }
  112. for (const v of versions) {
  113. collectVersions(v);
  114. }
  115. return versionsToAdd.reduceRight((historyGraph, version) => historyGraphReducer(historyGraph, {type: 'addVersion', version}), historyGraph);
  116. })
  117. }
  118. // // Idempotent
  119. // const appendDelta = (delta: CompositeDelta) => {
  120. // setState(({deltaGraphL0, deltaGraphL1, ...rest}) => {
  121. // return {
  122. // deltaGraphL1: deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta, active: false}),
  123. // deltaGraphL0: delta.reduce(
  124. // (graph, delta) => deltaGraphReducer(deltaGraphL0, {type: 'addDelta', delta, active: false}),
  125. // deltaGraphL0),
  126. // ...rest,
  127. // };
  128. // });
  129. // }
  130. const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
  131. const d3Updater = new D3GraphUpdater(setGraph, x, y);
  132. graphState.unexec(deltaToUndo, d3Updater);
  133. setDeltaGraphL0(deltaGraphL0 => deltaToUndo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaInactive', delta}), deltaGraphL0));
  134. setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}));
  135. };
  136. const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
  137. const d3Updater = new D3GraphUpdater(setGraph, x, y);
  138. graphState.exec(deltaToRedo, d3Updater);
  139. setDeltaGraphL0(deltaGraphL0 => deltaToRedo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaActive', delta}), deltaGraphL0));
  140. setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}));
  141. };
  142. const undo = (parentVersion, deltaToUndo) => {
  143. undoWithoutUpdatingHistoryGraph(deltaToUndo);
  144. currentVersion = parentVersion;
  145. setVersion(prevVersion => {
  146. setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
  147. {type: 'highlightVersion', version: prevVersion, bold: false}),
  148. {type: 'highlightVersion', version: parentVersion, bold: true}));
  149. return parentVersion;
  150. });
  151. };
  152. const redo = (childVersion, deltaToRedo) => {
  153. redoWithoutUpdatingHistoryGraph(deltaToRedo);
  154. currentVersion = childVersion;
  155. setVersion(prevVersion => {
  156. setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
  157. {type: 'highlightVersion', version: prevVersion, bold: false}),
  158. {type: 'highlightVersion', version: childVersion, bold: true}));
  159. return childVersion;
  160. });
  161. };
  162. const gotoVersion = (chosenVersion: Version) => {
  163. const path = currentVersion.findPathTo(chosenVersion);
  164. if (path === undefined) {
  165. throw new Error("Could not find path to version!");
  166. }
  167. for (const [linkType, delta] of path) {
  168. if (linkType === 'p') {
  169. undoWithoutUpdatingHistoryGraph(delta);
  170. }
  171. else if (linkType === 'c') {
  172. redoWithoutUpdatingHistoryGraph(delta);
  173. }
  174. }
  175. currentVersion = chosenVersion;
  176. setVersion(prevVersion => {
  177. setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
  178. {type: 'highlightVersion', version: prevVersion, bold: false}),
  179. {type: 'highlightVersion', version: chosenVersion, bold: true}));
  180. return chosenVersion;
  181. });
  182. };
  183. const reducer = {
  184. createVersion,
  185. gotoVersion,
  186. createAndGotoNewVersion,
  187. appendVersions,
  188. undo,
  189. redo,
  190. };
  191. // Components
  192. const defaultCallbacks = {
  193. onUserEdit: createAndGotoNewVersion,
  194. onUndoClicked: undo,
  195. onRedoClicked: redo,
  196. onVersionClicked: gotoVersion,
  197. onMerge: appendVersions,
  198. };
  199. const callbacks = Object.assign({}, defaultCallbacks, overridenCallbacks(reducer));
  200. const graphStateComponent = readonly ?
  201. <GraphView graphData={graph} help={helpText.graphEditorReadonly} mouseUpHandler={()=>{}} />
  202. : <InfoHoverCardOverlay contents={helpText.graphEditor}>
  203. <D3GraphEditable
  204. graph={graph}
  205. graphState={graphState}
  206. forces={defaultGraphForces}
  207. setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
  208. onUserEdit={callbacks.onUserEdit}
  209. />
  210. </InfoHoverCardOverlay>;
  211. const deltaComponentProps = {
  212. help: helpText.deltaGraph,
  213. defaultRenderer: ('graphviz' as Renderer),
  214. mouseUpHandler: (e, {x,y}, node) => {
  215. if (node) {
  216. alert(JSON.stringify(node.obj.serialize(), null, 2));
  217. }
  218. },
  219. };
  220. const deltaGraphL0Component = <GraphView<Delta,null> graphData={deltaGraphL0} {...deltaComponentProps} />;
  221. const deltaGraphL1Component = <GraphView<Delta,null> graphData={deltaGraphL1} {...deltaComponentProps} />;
  222. const historyComponentWithMerge = <MergeView
  223. history={historyGraph}
  224. forces={defaultGraphForces}
  225. versionRegistry={versionRegistry}
  226. onMerge={outputs => callbacks.onMerge?.(outputs)}
  227. onGoto={version => callbacks.onVersionClicked?.(version)}
  228. appendVersions={reducer.appendVersions}
  229. {...{primitiveRegistry, compositeLevel, createVersion}}
  230. />;
  231. const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
  232. <RountangleEditor
  233. graph={graph}
  234. graphState={graphState}
  235. onUserEdit={callbacks.onUserEdit}
  236. />
  237. </InfoHoverCardOverlay>;
  238. const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
  239. if (parentsOrChildren.length === 0) {
  240. return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
  241. }
  242. if (parentsOrChildren.length === 1) {
  243. return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
  244. }
  245. return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionProps={{duration:0}}>
  246. <Mantine.Menu.Target>
  247. <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
  248. </Mantine.Menu.Target>
  249. <Mantine.Menu.Dropdown>
  250. {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
  251. <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
  252. </Mantine.Menu.Dropdown>
  253. </Mantine.Menu>;
  254. }
  255. const undoButton = makeUndoOrRedoButton(version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
  256. const redoButton = makeUndoOrRedoButton(version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
  257. const undoRedoButtons = <>
  258. {undoButton}
  259. <Mantine.Space w="sm"/>
  260. {redoButton}
  261. </>;
  262. const stackedUndoButtons = version.parents.map(([parentVersion,deltaToUndo]) => {
  263. return (
  264. <div key={fullVersionId(parentVersion)}>
  265. <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconChevronLeft size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
  266. UNDO {deltaToUndo.getDescription()}
  267. </Mantine.Button>
  268. <Mantine.Space h="xs"/>
  269. </div>
  270. );
  271. });
  272. const stackedRedoButtons = version.children.map(([childVersion,deltaToRedo]) => {
  273. return (
  274. <div key={fullVersionId(childVersion)}>
  275. <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconChevronRight size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
  276. REDO {deltaToRedo.getDescription()}
  277. </Mantine.Button>
  278. <Mantine.Space h="xs"/>
  279. </div>
  280. );
  281. });
  282. const stackedUndoRedoButtons = (
  283. <Mantine.SimpleGrid cols={2}>
  284. <div>{stackedUndoButtons}</div>
  285. <div>{stackedRedoButtons}</div>
  286. </Mantine.SimpleGrid>
  287. );
  288. const makeTabs = (defaultTab: string, tabs: string[]) => {
  289. return <Mantine.Tabs defaultValue={defaultTab} keepMounted={false}>
  290. <Mantine.Tabs.List grow>
  291. {tabs.map(tab => ({
  292. editor: <Mantine.Tabs.Tab key={tab} value={tab}>Editor</Mantine.Tabs.Tab>,
  293. state: <Mantine.Tabs.Tab key={tab} value={tab}>State</Mantine.Tabs.Tab>,
  294. merge: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
  295. deltaL1: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L1)</Mantine.Tabs.Tab>,
  296. deltaL0: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L0)</Mantine.Tabs.Tab>,
  297. }[tab]))}
  298. </Mantine.Tabs.List>
  299. <Mantine.Tabs.Panel value="state">
  300. {graphStateComponent}
  301. </Mantine.Tabs.Panel>
  302. <Mantine.Tabs.Panel value="editor">
  303. {rountangleEditor}
  304. </Mantine.Tabs.Panel>
  305. <Mantine.Tabs.Panel value="deltaL1">
  306. {deltaGraphL1Component}
  307. </Mantine.Tabs.Panel>
  308. <Mantine.Tabs.Panel value="deltaL0">
  309. {deltaGraphL0Component}
  310. </Mantine.Tabs.Panel>
  311. <Mantine.Tabs.Panel value="merge">
  312. {historyComponentWithMerge}
  313. </Mantine.Tabs.Panel>
  314. </Mantine.Tabs>;
  315. }
  316. return {
  317. state: {
  318. version,
  319. },
  320. reducer,
  321. components: {
  322. graphStateComponent,
  323. rountangleEditor,
  324. deltaGraphL1Component,
  325. deltaGraphL0Component,
  326. historyComponentWithMerge,
  327. undoButton,
  328. redoButton,
  329. undoRedoButtons,
  330. stackedUndoRedoButtons,
  331. makeTabs,
  332. },
  333. };
  334. }
  335. return {
  336. graphState,
  337. useOnion,
  338. }
  339. }