merge_view.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import * as React from "react";
  2. import * as Mantine from "@mantine/core";
  3. import * as Icons from "@tabler/icons";
  4. import {Version} from "onion/version";
  5. import {Delta} from "onion/delta";
  6. import {DeltaParser} from "onion/delta_parser";
  7. import {VersionParser} from "onion/version_parser";
  8. import {genericGraphVizLayout, GraphView} from "./graph_view";
  9. import {fullVersionId, HistoryGraphState, historyGraphReducer} from "../d3graph/reducers/history_graph";
  10. import {InfoHoverCardOverlay} from "../info_hover_card";
  11. import {OnionContext, OnionContextType} from "../onion_context";
  12. const inputColor = 'seashell';
  13. const outputColor = 'lightblue';
  14. export const historyGraphHelpText = <>
  15. <Mantine.Divider label="Legend" labelPosition="center"/>
  16. <Mantine.Text>
  17. <b>Node</b>: Version<br/>
  18. <b>Arrow</b>: Parent-version-link<br/>
  19. Current version is <b>bold</b>.<br/>
  20. Selected versions are <b>yellow</b>.
  21. </Mantine.Text>
  22. <Mantine.Divider label="Controls" labelPosition="center"/>
  23. <Mantine.Text>
  24. <b>Left-Drag</b>: Drag Node<br/>
  25. <b>Right-Click</b>: Goto Version<br/>
  26. <Mantine.Kbd>S</Mantine.Kbd> + <b>Right-Click</b>: Select Version<br/>
  27. </Mantine.Text>
  28. </>;
  29. export const graphvizHelpText = <>
  30. <Mantine.Divider label="Legend" labelPosition="center"/>
  31. <Mantine.Text>
  32. <b>Node</b>: Version<br/>
  33. <b>Arrow</b>: Parent-version-link<br/>
  34. Current version is <b>bold</b>.<br/>
  35. Selected versions are <b>yellow</b>.
  36. </Mantine.Text>
  37. <Mantine.Divider label="Controls" labelPosition="center"/>
  38. <Mantine.Text>
  39. <b>Left-Click</b>: Goto Version<br/>
  40. <Mantine.Kbd>S</Mantine.Kbd> + <b>Left-Click</b>: Select Version<br/>
  41. </Mantine.Text>
  42. </>
  43. type EmbeddingTreeNode = {
  44. visible: boolean;
  45. children: Array<[string, EmbeddingTreeNode]>,
  46. };
  47. export function MergeView({history, forces, versionRegistry, onMerge, onGoto, deltaRegistry, appendVersions, appendDelta}) {
  48. const [inputs, setInputs] = React.useState<Version[]>([]);
  49. const [outputs, setOutputs] = React.useState<Version[]>([]);
  50. const [showTooltip, setShowTooltip] = React.useState<any>(null);
  51. const [selectMode, setSelectMode] = React.useState<boolean>(false);
  52. const [visibleEmbeddings, setVisibleEmbeddings] = React.useState<Array<[string, boolean]>>([]);
  53. // Figure out all the 'guestId's in the history graph:
  54. const allEmbeddings = React.useMemo(() => {
  55. const all = new Set<string>();
  56. for (const n of history.nodes) {
  57. for (const [guestId] of n.obj.reverseEmbeddings.entries()) {
  58. // here we get every possible new 'guestId'
  59. all.add(guestId);
  60. }
  61. }
  62. return all;
  63. }, [history]);
  64. // Update our state of visible/hidden embeddings according to all the guestIds in the history graph:
  65. React.useEffect(() => {
  66. const toAdd: Array<string> = [];
  67. for (const guestId of allEmbeddings) {
  68. // here we get every possible new 'guestId'
  69. if (!visibleEmbeddings.find(([gId]) => gId === guestId)) {
  70. toAdd.push(guestId);
  71. }
  72. }
  73. const filtered = visibleEmbeddings
  74. .filter(([guestId]) => allEmbeddings.has(guestId))
  75. .concat(toAdd.map(x => [x,true] as [string,boolean]));
  76. setVisibleEmbeddings(filtered);
  77. }, [allEmbeddings]);
  78. // TODO: better to move the style of D3Graph nodes/links to a separate (React state) data structure.
  79. // An update of the style will then not trigger an update of the layout.
  80. const historyHighlightedInputs = inputs.reduce(
  81. (history, version) =>
  82. historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: inputColor}),
  83. history);
  84. const historyHighlighted = outputs.reduce(
  85. (history, version) =>
  86. historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: outputColor}),
  87. historyHighlightedInputs);
  88. const historyFiltered = visibleEmbeddings.reduce(
  89. (history, [guestId], i) => {
  90. if (visibleEmbeddings[i][1]) {
  91. return history;
  92. }
  93. else {
  94. const filteredNodes = history.nodes.filter(n => n.obj.reverseEmbeddings.get(guestId) === undefined);
  95. return {
  96. nodes: filteredNodes,
  97. links: history.links.filter(l => filteredNodes.some(n => n.obj === l.source.obj)
  98. && filteredNodes.some(n => n.obj === l.target.obj)),
  99. };
  100. }
  101. },
  102. historyHighlighted);
  103. const removeButton = version => (
  104. <Mantine.ActionIcon size="xs" color="dark" radius="xl" variant="transparent" onClick={() => {
  105. setInputs(inputs.filter(v => v !== version));
  106. setOutputs([]);
  107. }}>
  108. <Icons.IconX size={10} />
  109. </Mantine.ActionIcon>
  110. );
  111. function onKeyEvent(e) {
  112. if (e.key === "s") {
  113. setSelectMode(e.type === "keydown");
  114. }
  115. }
  116. React.useEffect(() => {
  117. window.addEventListener("keydown", onKeyEvent);
  118. window.addEventListener("keyup", onKeyEvent);
  119. return () => {
  120. window.removeEventListener("keydown", onKeyEvent);
  121. window.removeEventListener("keyup", onKeyEvent);
  122. }
  123. }, [])
  124. return <>
  125. <GraphView<Version,Delta> graphData={historyFiltered}
  126. graphvizLayout={genericGraphVizLayout}
  127. help={historyGraphHelpText}
  128. graphvizHelp={graphvizHelpText}
  129. mouseUpHandler={(e, {x, y}, node) => {
  130. if (node !== undefined) {
  131. if (selectMode) {
  132. if (inputs.includes(node.obj)) {
  133. // remove from inputs
  134. setInputs(inputs => inputs.filter(v => v !== node.obj));
  135. }
  136. else {
  137. // add to inputs
  138. setInputs(inputs => inputs.concat(node.obj));
  139. }
  140. setOutputs([]);
  141. }
  142. else {
  143. onGoto(node.obj);
  144. }
  145. }
  146. }}
  147. defaultRenderer="graphviz">
  148. <Mantine.Switch checked={selectMode} onChange={e => setSelectMode(e.currentTarget.checked)} label={<><Mantine.Kbd>S</Mantine.Kbd>elect</>}/>
  149. {visibleEmbeddings.map(([guestId, visible], i) =>
  150. <Mantine.Checkbox key={guestId} label={guestId} checked={visible}
  151. onChange={event => {
  152. const vCloned = visibleEmbeddings.slice();
  153. vCloned[i] = [guestId, event.currentTarget.checked];
  154. setVisibleEmbeddings(vCloned);
  155. }}
  156. />
  157. )}
  158. </GraphView>
  159. <Mantine.Group style={{minHeight: 30}}>
  160. {inputs.map(version => <Mantine.Badge key={fullVersionId(version)} pr={3} variant="outline" color="dark" style={{backgroundColor: inputColor}} rightSection={removeButton(version)}>
  161. {fullVersionId(version).slice(0,8)}
  162. </Mantine.Badge>)}
  163. { outputs.length === 0 ? <></> :
  164. <>
  165. <Icons.IconArrowNarrowRight/>
  166. {outputs.map(version => <Mantine.Badge key={fullVersionId(version)} variant="outline" color="dark"
  167. style={{backgroundColor: outputColor, cursor: "pointer"}} onClick={() => {
  168. setInputs([version]);
  169. setOutputs([]);
  170. }}>
  171. {fullVersionId(version).slice(0,8)}
  172. </Mantine.Badge>)}
  173. </> }
  174. </Mantine.Group>
  175. <Mantine.Group grow>
  176. <Mantine.Button compact variant="outline" leftIcon={<Icons.IconX/>} disabled={inputs.length===0 && outputs.length===0} onClick={() => {
  177. setInputs([]);
  178. setOutputs([]);
  179. }}>Clear Selection</Mantine.Button>
  180. <Mantine.Button compact leftIcon={<Icons.IconArrowMerge/>} onClick={() => {
  181. const outputs = versionRegistry.crazyMerge(inputs, d => d.description);
  182. setOutputs(outputs);
  183. onMerge(outputs);
  184. }}>Merge</Mantine.Button>
  185. <Mantine.Button compact leftIcon={<Icons.IconDatabaseImport/>} onClick={() => {
  186. let parsed;
  187. while (true) {
  188. const toImport = prompt("Versions to import (JSON)", "[]");
  189. if (toImport === null) {
  190. return; // 'cancel'
  191. }
  192. try {
  193. parsed = JSON.parse(toImport);
  194. break;
  195. } catch (e) {
  196. alert("Invalid JSON");
  197. }
  198. // ask again ...
  199. }
  200. const deltaParser = new DeltaParser(deltaRegistry);
  201. const versionParser = new VersionParser(deltaParser, versionRegistry);
  202. versionParser.load(parsed, delta => {
  203. appendDelta(delta);
  204. }, version => {
  205. appendVersions([version]);
  206. });
  207. }}
  208. >Import</Mantine.Button>
  209. <Mantine.Tooltip label="Copied to clipboard!" opened={showTooltip !== null} withArrow>
  210. <Mantine.Button compact leftIcon={<Icons.IconDatabaseExport/>} disabled={inputs.length!==1} onClick={() => {
  211. const type = "application/json";
  212. const blob = new Blob([], {type});
  213. console.log(inputs[0].serialize());
  214. navigator.clipboard.writeText(
  215. JSON.stringify(inputs[0].serialize(), null, 2)
  216. );
  217. if (showTooltip !== null) clearTimeout(showTooltip);
  218. setShowTooltip(setTimeout(()=>setShowTooltip(null), 1500));
  219. }}>Export</Mantine.Button>
  220. </Mantine.Tooltip>
  221. </Mantine.Group>
  222. </>
  223. }