Bläddra i källkod

Implemented merge view

Joeri Exelmans 2 år sedan
förälder
incheckning
6f9421d72d

+ 3 - 3
src/frontend/d3graph/d3graph.tsx

@@ -12,7 +12,7 @@ export type D3NodeData<NodeType> = {
   obj: NodeType;
   x?: number,
   y?: number,
-  highlight: boolean,
+  bold: boolean,
 };
 
 export type D3LinkData<LinkType> = {
@@ -173,7 +173,7 @@ class D3Node<NodeType> extends React.Component<D3NodeProps<NodeType>, D3NodeStat
         onMouseUp={this.props.mouseUpHandler}
         r={5}
         fill={this.props.node.color}
-        style={{strokeWidth:this.props.node.highlight?"3px":"1px"}}
+        style={{strokeWidth:this.props.node.bold?"3px":"1px"}}
       >
         <title>{this.props.node.id}</title>
       </circle>
@@ -238,7 +238,7 @@ class D3Label<NodeType> extends React.Component<{ node: D3NodeData<NodeType> },
   }
 
   render() {
-    return <text className="graphNodeLabel" ref={this.ref} fontWeight={this.props.node.highlight?"bold":"normal"}>
+    return <text className="graphNodeLabel" ref={this.ref} fontWeight={this.props.node.bold?"bold":"normal"}>
       {this.props.node.label}
     </text>;
   }

+ 4 - 4
src/frontend/d3graph/reducers/delta_graph.ts

@@ -57,7 +57,7 @@ export function deltaGraphReducer(prevState: DeltaGraphState, action: DeltaGraph
       }
       return {
         // add one extra node that represents the new delta:
-        nodes: prevState.nodes.concat(deltaToDepGraphNode(delta, /*highlight: */ action.active)),
+        nodes: prevState.nodes.concat(deltaToDepGraphNode(delta, /* active: */ action.active)),
         // for every dependency and conflict, add a link:
         links: prevState.links.concat(
             ...delta.getTypedDependencies().map(([dep,depSummary]) => dependencyToDepGraphLink(delta, dep, depSummary)),
@@ -69,17 +69,17 @@ export function deltaGraphReducer(prevState: DeltaGraphState, action: DeltaGraph
 }
 
 export function fullDeltaId(delta: Delta): string {
-  return delta.getHash().toString('base64');
+  return delta.getHash().toString('hex');
 }
 
 // Helpers
 
-function deltaToDepGraphNode(delta: Delta, highlight: boolean, x?: number, y?: number): D3NodeData<Delta> {
+function deltaToDepGraphNode(delta: Delta, active: boolean, x?: number, y?: number): D3NodeData<Delta> {
   return {
     id: fullDeltaId(delta),
     label: delta.getDescription(),
     color: getDeltaColor(delta),
-    highlight,
+    bold: active,
     obj: delta,
     x, y,
   };

+ 33 - 29
src/frontend/d3graph/reducers/history_graph.ts

@@ -2,45 +2,49 @@ import {Version} from "onion/version";
 import {Delta} from "onion/delta";
 import {D3GraphData, D3NodeData, D3LinkData} from "../d3graph";
 
-export type HistoryGraphState = D3GraphData<Version|null,Delta>;
+export type HistoryGraphState = D3GraphData<Version,Delta>;
 
 interface AppendToHistoryGraph {type:'addVersion', version: Version}
-interface SetCurrentVersion {type:'setCurrentVersion', prev: Version, new: Version}
+interface SetCurrentVersion {type:'highlightVersion', version: Version, bold: boolean, overrideColor?: string}
 
 export type HistoryGraphAction = Readonly<SetCurrentVersion> | Readonly<AppendToHistoryGraph>;
 
 export function historyGraphReducer(prevState: HistoryGraphState, action: HistoryGraphAction): HistoryGraphState {
   switch (action.type) {
     case 'addVersion': {
-      const newLinks = action.version.parents.map(([parentVersion,delta]) => parentLinkToHistoryGraphLink(action.version, parentVersion, delta)).filter(link => !prevState.links.some(prevLink => prevLink.source.id === link.source && prevLink.target.id === link.target));
+      const nodes = prevState.nodes.some(node => node.id === fullVersionId(action.version)) ?
+            prevState.nodes : prevState.nodes.concat(versionToNode(action.version, false));
+      const newLinks = action.version.parents.map(([parentVersion,delta]) => parentLinkToHistoryGraphLink(action.version, parentVersion, delta, nodes))
+        // don't add links that are already there:
+        .filter(link => !prevState.links.some(prevLink => (prevLink.source.id === link.source
+                                                        || prevLink.source === link.source)
+                                                       && (prevLink.target.id === link.target
+                                                        || prevLink.target === link.target)));
 
       return {
-        nodes: prevState.nodes.some(node => node.id === fullVersionId(action.version)) ?
-            prevState.nodes : prevState.nodes.concat(versionToNode(action.version, false)),
+        nodes,
         links: prevState.links.concat(...newLinks),
       };
     }
-    case 'setCurrentVersion': {
-      if (action.prev === action.new) {
-        return prevState;
-      }
-      let links;
-      return {
-        nodes: prevState.nodes.map(n => {
-          if (n.obj === action.prev) {
-            return versionToNode(action.prev, false, n.x, n.y);
-          }
-          if (n.obj === action.new) {
-            return versionToNode(action.new, true, n.x, n.y);
-          }
+    case 'highlightVersion': {
+      const nodes = prevState.nodes.map(n => {
+        if (n.obj === action.version) {
+          return versionToNode(action.version, action.bold, n.x, n.y, action.overrideColor);
+        }
+        else {
           return n;
-        }),
+        }
+      });
+      return {
+        nodes,
         // must re-create links for re-created nodes:
         links: prevState.links.map(l => {
-          if (l.source.obj === action.prev || l.target.obj === action.prev || l.source.obj === action.new || l.target.obj === action.new) {
-            return parentLinkToHistoryGraphLink(l.source.obj, l.target.obj, l.obj);
+          if (l.source.obj === action.version || l.target.obj === action.version) {
+            return parentLinkToHistoryGraphLink(l.source.obj, l.target.obj, l.obj, nodes);
+          }
+          else {
+            return l;
           }
-          return l;
         }),
       }
     }
@@ -57,26 +61,26 @@ export function initialHistoryGraph(initialVersion) {
 }
 
 export function fullVersionId(version: Version): string {
-  return version.hash.toString('base64');
+  return version.hash.toString('hex');
 }
 
 // Helpers
 
-function versionToNode(version: Version, highlight: boolean, x?: number, y?: number): D3NodeData<Version> {
+function versionToNode(version: Version, bold: boolean, x?: number, y?: number, overrideColor?: string): D3NodeData<Version> {
   return {
     id: fullVersionId(version),
     label: (version.parents.length === 0 ? "initial" : ""),
-    color: (version.parents.length === 0 ? "grey" : "purple"),
+    color: overrideColor ? overrideColor : (version.parents.length === 0 ? "grey" : "purple"),
     obj: version,
-    highlight,
+    bold,
     x, y,
   }
 }
-function parentLinkToHistoryGraphLink(childVersion: Version, parentVersion: Version, delta: Delta): D3LinkData<Delta> {
+function parentLinkToHistoryGraphLink(childVersion: Version, parentVersion: Version, delta: Delta, nodes): D3LinkData<Delta> {
   return {
-    source: fullVersionId(childVersion),
+    source: nodes.find(n => n.obj === childVersion),
     label: delta.getDescription(),
-    target: fullVersionId(parentVersion),
+    target: nodes.find(n => n.obj === parentVersion),
     color: 'black',
     obj: delta,
   };

+ 2 - 2
src/frontend/d3graph/reducers/onion_graph.ts

@@ -36,7 +36,7 @@ export function onionGraphReducer(prevState: D3OnionGraphData, action: D3OnionGr
           y: action.y,
           color: "darkturquoise",
           obj: action.ns,
-          highlight: false,
+          bold: false,
         }],
         links: prevState.links,
       };
@@ -56,7 +56,7 @@ export function onionGraphReducer(prevState: D3OnionGraphData, action: D3OnionGr
           y: action.y,
           color: "darkorange",
           obj: action.vs,
-          highlight: false,
+          bold: false,
         }],
         links: prevState.links,
       };

+ 38 - 35
src/frontend/demos/demo_le.tsx

@@ -8,6 +8,7 @@ import {PrimitiveValue} from "onion/types";
 import {INodeState, IValueState} from "onion/graph_state";
 
 import {newVersionedModel, VersionedModelState} from "../versioned_model/single_model";
+import {MergeView} from "../versioned_model/merge_view";
 import {InfoHoverCard} from "../info_hover_card";
 import {OnionContext} from "../onion_context";
 import {useConst} from "../use_const";
@@ -38,8 +39,8 @@ export function getDemoLE() {
 
     // returns functional react component
     return function () {
-        const [modelState, setCsState] = React.useState<VersionedModelState>(model.initialState);
-        const modelReducer = model.getReducer(setCsState);
+        const [modelState, setModelState] = React.useState<VersionedModelState>(model.initialState);
+        const modelReducer = model.getReducer(setModelState);
 
         React.useEffect(() => {
             // idempotent:
@@ -110,10 +111,11 @@ export function getDemoLE() {
             onUndoClicked: modelReducer.undo,
             onRedoClicked: modelReducer.redo,
             onVersionClicked: modelReducer.gotoVersion,
+            onMerge: outputs => {
+                outputs.forEach(version => modelReducer.appendVersion(version));
+            },
         });
 
-        const deltaTabs = ["deltaL1", "deltaL0", "history"];
-
         const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
 
         // Recursively renders list elements as a vertical stack
@@ -138,18 +140,8 @@ export function getDemoLE() {
             <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
                 <SimpleGrid cols={3}>
                     <div>
-                        <Group position="apart">
-                            <Title order={4}>List Editor</Title>
-                        </Group>
-                        <Space h="48px"/>
                         <Stack>
-                            <Center>
-                                {modelComponents.undoRedoButtons}
-                                <Space w="sm"/>
-                                <InfoHoverCard>
-                                    {undoButtonHelpText}
-                                </InfoHoverCard>
-                            </Center>
+                            <Title order={5}>List Editor</Title>
                             {
                             listNode === undefined ?
                                 <Alert icon={<IconAlertCircle/>} title="There exists no list!" radius="md">
@@ -163,28 +155,39 @@ export function getDemoLE() {
                         </Stack>
                     </div>
                     <div>
-                        <Group position="center">
-                            <Title order={4}>Deltas</Title>
-                        </Group>
-                        <Space h="sm"/>
-                        {modelComponents.makeTabs("deltaL1", deltaTabs)}
+                        <Stack>
+                            <Title order={5}>History</Title>
+                            <Center>
+                                {modelComponents.undoRedoButtons}
+                                <Space w="sm"/>
+                                <InfoHoverCard>
+                                    {undoButtonHelpText}
+                                </InfoHoverCard>
+                            </Center>
+                            {modelComponents.historyComponentWithMerge}
+                            <Title order={5}>Deltas</Title>
+                            {modelComponents.makeTabs("deltaL1", ["deltaL1", "deltaL0"])}
+                        </Stack>
                     </div>
                     <div>
-                        <Title order={4}>State Graph (read-only)</Title>
-                        <Space h="48px"/>
-                        {modelComponents.graphStateComponent}
-                        Nodes:
-                        <ul>
-                            {modelState.graph.nodes.map(n => {
-                                return <li key={n.id}>{n.id}</li>;
-                            })}
-                        </ul>
-                        Edges:
-                        <ul>
-                            {modelState.graph.links.map(e => {
-                                return <li key={e.source.id+"-"+e.label+"->"+e.target.id}>{e.source.id} ---{e.label}--&gt; {e.target.id}</li>
-                            })}
-                        </ul>
+                        <Stack>
+                            <Title order={5}>State Graph (read-only)</Title>
+                            {modelComponents.graphStateComponent}
+                            <div>
+                            Nodes:
+                            <ul>
+                                {modelState.graph.nodes.map(n => {
+                                    return <li key={n.id}>{n.id}</li>;
+                                })}
+                            </ul>
+                            Edges:
+                            <ul>
+                                {modelState.graph.links.map(e => {
+                                    return <li key={e.source.id+"-"+e.label+"->"+e.target.id}>{e.source.id} ---{e.label}--&gt; {e.target.id}</li>
+                                })}
+                            </ul>
+                            </div>
+                        </Stack>
                     </div>
                 </SimpleGrid>
             </OnionContext.Provider>

+ 99 - 0
src/frontend/versioned_model/merge_view.tsx

@@ -0,0 +1,99 @@
+import * as React from "react";
+import * as Mantine from "@mantine/core";
+import * as Icons from "@tabler/icons";
+
+import {Version} from "onion/version";
+import {Delta} from "onion/delta";
+import {D3Graph} from "../d3graph/d3graph";
+import {fullVersionId, HistoryGraphState, historyGraphReducer} from "../d3graph/reducers/history_graph";
+import {InfoHoverCardOverlay} from "../info_hover_card";
+
+export const historyGraphHelpText = <>
+  <Mantine.Divider label="Legend" labelPosition="center"/>
+  <Mantine.Text>
+    <b>Node</b>: Version<br/>
+    <b>Arrow</b>: Parent-version-link<br/>
+    Current version is <b>bold</b>.<br/>
+    Selected versions are <b>white</b>.
+  </Mantine.Text>
+  <Mantine.Divider label="Controls" labelPosition="center"/>
+  <Mantine.Text>
+    <b>Left-Drag</b>: Drag Node<br/>
+    <b>Middle-Click</b>: Goto Version<br/>
+    <b>Right-Click</b>: Select/Unselect Version<br/>
+  </Mantine.Text>
+</>;
+
+export function MergeView({history, forces, versionRegistry, onMerge, onGoto}) {
+  const [inputs, setInputs] = React.useState<Version[]>([]);
+  const [outputs, setOutputs] = React.useState<Version[]>([]);
+
+  const historyHighlightedInputs = inputs.reduce(
+    (history, version) =>
+      historyGraphReducer(history, {type: 'highlightVersion', version, bold: false, overrideColor: 'white'}),
+    history);
+  const historyHighlighted = outputs.reduce(
+    (history, version) =>
+      historyGraphReducer(history, {type: 'highlightVersion', version, bold: false, overrideColor: 'yellow'}),
+    historyHighlightedInputs);
+
+  const removeButton = version => (
+    <Mantine.ActionIcon size="xs" color="dark" radius="xl" variant="transparent" onClick={() => {
+      setInputs(inputs.filter(v => v !== version));
+      setOutputs([]);
+    }}>
+      <Icons.IconX size={10} />
+    </Mantine.ActionIcon>
+  );
+
+  return <>
+    <InfoHoverCardOverlay contents={historyGraphHelpText}>
+      <D3Graph<Version,Delta>
+        graph={historyHighlighted}
+        forces={forces}
+        mouseUpHandler={(e, {x, y}, node) => {
+          if (node !== undefined) {
+            // @ts-ignore:
+            if (e.button === 2) { // right mouse button
+              if (inputs.includes(node.obj)) {
+                setInputs(inputs.filter(v => v !== node.obj));
+              }
+              else {
+                setInputs(inputs.concat(node.obj));
+              }
+              setOutputs([]);
+            }
+            // @ts-ignore:
+            else if (e.button === 1) { // middle mouse button
+              onGoto(node.obj);
+            }
+          }
+        }}
+      />
+    </InfoHoverCardOverlay>
+    <Mantine.Group>
+      { inputs.length === 0 ? <></> :
+        <>
+          {inputs.map(version => <Mantine.Badge key={fullVersionId(version)} pr={3} variant="outline" color="dark" rightSection={removeButton(version)}>
+            {fullVersionId(version).slice(0,8)}
+            </Mantine.Badge>)}
+        </> }
+      { outputs.length === 0 ? <></> :
+        <>
+          <Icons.IconArrowNarrowRight/>
+          {/*Merge Outputs:*/}
+          {outputs.map(version => <Mantine.Badge key={fullVersionId(version)} variant="outline" color="dark" style={{backgroundColor: 'yellow'}}>
+            {fullVersionId(version).slice(0,8)}
+            </Mantine.Badge>)}
+        </> }
+    </Mantine.Group>
+    <Mantine.Group grow>
+      <Mantine.Button compact variant="light" disabled={inputs.length===0 && outputs.length===0} onClick={() => {setInputs([]); setOutputs([]);}}>Clear Selection</Mantine.Button>
+      <Mantine.Button compact leftIcon={<Icons.IconArrowMerge/>} disabled={inputs.length===0} onClick={() => {
+        const outputs = versionRegistry.merge(inputs);
+        setOutputs(outputs);
+        onMerge(outputs);
+      }}>Merge</Mantine.Button>
+    </Mantine.Group>
+  </>
+}

+ 32 - 11
src/frontend/versioned_model/single_model.tsx

@@ -19,6 +19,7 @@ import {
 } from "../d3graph/reducers/history_graph";
 
 import * as helpText from "./help_text";
+import {MergeView} from "./merge_view";
 
 import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
 import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
@@ -46,6 +47,7 @@ interface VersionedModelCallbacks {
   onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void;
   onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void;
   onVersionClicked?: (Version) => void;
+  onMerge?: (outputs: Version[]) => void;
 }
 
 // Basically everything we need to construct the React components for:
@@ -85,13 +87,7 @@ export function newVersionedModel({readonly}) {
 
     // Create and add a new version, and its deltas, without changing the current version
     const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
-      let composite;
-      // try {
-        composite = compositeLevel.createComposite(deltas, description);
-      // } catch(e) {
-        
-      //   return;
-      // }
+      const composite = compositeLevel.createComposite(deltas, description);
 
       const parentVersion = versionRegistry.lookupOptional(parentHash);
       if (parentVersion !== undefined) {
@@ -99,7 +95,7 @@ export function newVersionedModel({readonly}) {
 
         setState(({historyGraph, deltaGraphL1, deltaGraphL0, ...rest}) => {
           return {
-            // add new version to history graph + highlight the new version as the current version:
+            // add new version to history graph:
             historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}),
             // add the composite delta to the L1-graph + highlight it as 'active':
             deltaGraphL1: composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1, // never add an empty composite
@@ -119,6 +115,15 @@ export function newVersionedModel({readonly}) {
       gotoVersion(newVersion);
       return newVersion;
     };
+    const appendVersion = (version: Version) => {
+      setState(({historyGraph, ...rest}) => {
+        return {
+          // add new version to history graph:
+          historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version}),
+          ...rest,
+        };
+      });
+    }
 
     // helper
     const setGraph = callback =>
@@ -148,7 +153,9 @@ export function newVersionedModel({readonly}) {
       currentVersion = parentVersion;
       setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
         version: parentVersion,
-        historyGraph: historyGraphReducer(prevHistoryGraph, {type: 'setCurrentVersion', prev: prevVersion, new: parentVersion}),
+        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
+            {type: 'highlightVersion', version: prevVersion, bold: false}),
+            {type: 'highlightVersion', version: parentVersion, bold: true}),
         ...rest,
       }));
     };
@@ -157,7 +164,9 @@ export function newVersionedModel({readonly}) {
       currentVersion = childVersion;
       setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
         version: childVersion,
-        historyGraph: historyGraphReducer(prevHistoryGraph, {type: 'setCurrentVersion', prev: prevVersion, new: childVersion}),
+        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
+          {type: 'highlightVersion', version: prevVersion, bold: false}),
+          {type: 'highlightVersion', version: childVersion, bold: true}),
         ...rest,
       }));
     };
@@ -177,7 +186,9 @@ export function newVersionedModel({readonly}) {
       currentVersion = chosenVersion;
       setState(({historyGraph, version: oldVersion, ...rest}) => ({
         version: chosenVersion,
-        historyGraph: historyGraphReducer(historyGraph, {type: 'setCurrentVersion', prev: oldVersion, new: chosenVersion}),
+        historyGraph: historyGraphReducer(historyGraphReducer(historyGraph,
+          {type: 'highlightVersion', version: oldVersion, bold: false}),
+          {type: 'highlightVersion', version: chosenVersion, bold: true}),
         ...rest,
       }));
     };
@@ -186,6 +197,7 @@ export function newVersionedModel({readonly}) {
       addDeltasAndVersion,
       gotoVersion,
       createAndGotoNewVersion,
+      appendVersion,
       undo,
       redo,
     };
@@ -218,6 +230,13 @@ export function newVersionedModel({readonly}) {
         mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />
     </InfoHoverCardOverlay>;
 
+    const historyComponentWithMerge = <MergeView
+      history={state.historyGraph}
+      forces={defaultGraphForces}
+      versionRegistry={versionRegistry}
+      onMerge={outputs => callbacks.onMerge?.(outputs)}
+      onGoto={version => callbacks.onVersionClicked?.(version)} />
+
     const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
       <RountangleEditor
         graph={state.graph}
@@ -316,6 +335,7 @@ export function newVersionedModel({readonly}) {
       deltaGraphL1Component,
       deltaGraphL0Component,
       historyComponent,
+      historyComponentWithMerge,
       undoButton,
       redoButton,
       undoRedoButtons,
@@ -328,6 +348,7 @@ export function newVersionedModel({readonly}) {
   return {
     initialState,
     graphState,
+    versionRegistry,
     getCurrentVersion,
     getReducer,
     getReactComponents,