123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- 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 {DeltaParser} from "onion/delta_parser";
- import {VersionParser} from "onion/version_parser";
- import {genericGraphVizLayout, GraphView} from "./graph_view";
- import {fullVersionId, HistoryGraphState, historyGraphReducer} from "../d3graph/reducers/history_graph";
- import {InfoHoverCardOverlay} from "../info_hover_card";
- import {OnionContext, OnionContextType} from "../onion_context";
- const inputColor = 'seashell';
- const outputColor = 'lightblue';
- 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>yellow</b>.
- </Mantine.Text>
- <Mantine.Divider label="Controls" labelPosition="center"/>
- <Mantine.Text>
- <b>Left-Drag</b>: Drag Node<br/>
- <b>Right-Click</b>: Goto Version<br/>
- <Mantine.Kbd>S</Mantine.Kbd> + <b>Right-Click</b>: Select Version<br/>
- </Mantine.Text>
- </>;
- export const graphvizHelpText = <>
- <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>yellow</b>.
- </Mantine.Text>
- <Mantine.Divider label="Controls" labelPosition="center"/>
- <Mantine.Text>
- <b>Left-Click</b>: Goto Version<br/>
- <Mantine.Kbd>S</Mantine.Kbd> + <b>Left-Click</b>: Select Version<br/>
- </Mantine.Text>
- </>
- type EmbeddingTreeNode = {
- visible: boolean;
- children: Array<[string, EmbeddingTreeNode]>,
- };
- export function MergeView({history, forces, versionRegistry, onMerge, onGoto, deltaRegistry, appendVersions, appendDelta}) {
- const [inputs, setInputs] = React.useState<Version[]>([]);
- const [outputs, setOutputs] = React.useState<Version[]>([]);
- const [showTooltip, setShowTooltip] = React.useState<any>(null);
- const [selectMode, setSelectMode] = React.useState<boolean>(false);
- const [visibleEmbeddings, setVisibleEmbeddings] = React.useState<Array<[string, boolean]>>([]);
- // Figure out all the 'guestId's in the history graph:
- const allEmbeddings = React.useMemo(() => {
- const all = new Set<string>();
- for (const n of history.nodes) {
- for (const [guestId] of n.obj.reverseEmbeddings.entries()) {
- // here we get every possible new 'guestId'
- all.add(guestId);
- }
- }
- return all;
- }, [history]);
- // Update our state of visible/hidden embeddings according to all the guestIds in the history graph:
- React.useEffect(() => {
- const toAdd: Array<string> = [];
- for (const guestId of allEmbeddings) {
- // here we get every possible new 'guestId'
- if (!visibleEmbeddings.find(([gId]) => gId === guestId)) {
- toAdd.push(guestId);
- }
- }
- const filtered = visibleEmbeddings
- .filter(([guestId]) => allEmbeddings.has(guestId))
- .concat(toAdd.map(x => [x,true] as [string,boolean]));
- setVisibleEmbeddings(filtered);
- }, [allEmbeddings]);
- // TODO: better to move the style of D3Graph nodes/links to a separate (React state) data structure.
- // An update of the style will then not trigger an update of the layout.
- const historyHighlightedInputs = inputs.reduce(
- (history, version) =>
- historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: inputColor}),
- history);
- const historyHighlighted = outputs.reduce(
- (history, version) =>
- historyGraphReducer(history, {type: 'highlightVersion', version, overrideColor: outputColor}),
- historyHighlightedInputs);
- const historyFiltered = visibleEmbeddings.reduce(
- (history, [guestId], i) => {
- if (visibleEmbeddings[i][1]) {
- return history;
- }
- else {
- const filteredNodes = history.nodes.filter(n => n.obj.reverseEmbeddings.get(guestId) === undefined);
- return {
- nodes: filteredNodes,
- links: history.links.filter(l => filteredNodes.some(n => n.obj === l.source.obj)
- && filteredNodes.some(n => n.obj === l.target.obj)),
- };
- }
- },
- historyHighlighted);
- 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>
- );
- function onKeyEvent(e) {
- if (e.key === "s") {
- setSelectMode(e.type === "keydown");
- }
- }
- React.useEffect(() => {
- window.addEventListener("keydown", onKeyEvent);
- window.addEventListener("keyup", onKeyEvent);
- return () => {
- window.removeEventListener("keydown", onKeyEvent);
- window.removeEventListener("keyup", onKeyEvent);
- }
- }, [])
- return <>
- <GraphView<Version,Delta> graphData={historyFiltered}
- graphvizLayout={genericGraphVizLayout}
- help={historyGraphHelpText}
- graphvizHelp={graphvizHelpText}
- mouseUpHandler={(e, {x, y}, node) => {
- if (node !== undefined) {
- if (selectMode) {
- if (inputs.includes(node.obj)) {
- // remove from inputs
- setInputs(inputs => inputs.filter(v => v !== node.obj));
- }
- else {
- // add to inputs
- setInputs(inputs => inputs.concat(node.obj));
- }
- setOutputs([]);
- }
- else {
- onGoto(node.obj);
- }
- }
- }}
- defaultRenderer="graphviz">
- <Mantine.Switch checked={selectMode} onChange={e => setSelectMode(e.currentTarget.checked)} label={<><Mantine.Kbd>S</Mantine.Kbd>elect</>}/>
- {visibleEmbeddings.map(([guestId, visible], i) =>
- <Mantine.Checkbox key={guestId} label={guestId} checked={visible}
- onChange={event => {
- const vCloned = visibleEmbeddings.slice();
- vCloned[i] = [guestId, event.currentTarget.checked];
- setVisibleEmbeddings(vCloned);
- }}
- />
- )}
- </GraphView>
- <Mantine.Group style={{minHeight: 30}}>
- {inputs.map(version => <Mantine.Badge key={fullVersionId(version)} pr={3} variant="outline" color="dark" style={{backgroundColor: inputColor}} rightSection={removeButton(version)}>
- {fullVersionId(version).slice(0,8)}
- </Mantine.Badge>)}
- { outputs.length === 0 ? <></> :
- <>
- <Icons.IconArrowNarrowRight/>
- {outputs.map(version => <Mantine.Badge key={fullVersionId(version)} variant="outline" color="dark"
- style={{backgroundColor: outputColor, cursor: "pointer"}} onClick={() => {
- setInputs([version]);
- setOutputs([]);
- }}>
- {fullVersionId(version).slice(0,8)}
- </Mantine.Badge>)}
- </> }
- </Mantine.Group>
- <Mantine.Group grow>
- <Mantine.Button compact variant="outline" leftIcon={<Icons.IconX/>} disabled={inputs.length===0 && outputs.length===0} onClick={() => {
- setInputs([]);
- setOutputs([]);
- }}>Clear Selection</Mantine.Button>
- <Mantine.Button compact leftIcon={<Icons.IconArrowMerge/>} onClick={() => {
- const outputs = versionRegistry.crazyMerge(inputs, d => d.description);
- setOutputs(outputs);
- onMerge(outputs);
- }}>Merge</Mantine.Button>
- <Mantine.Button compact leftIcon={<Icons.IconDatabaseImport/>} onClick={() => {
- let parsed;
- while (true) {
- const toImport = prompt("Versions to import (JSON)", "[]");
- if (toImport === null) {
- return; // 'cancel'
- }
- try {
- parsed = JSON.parse(toImport);
- break;
- } catch (e) {
- alert("Invalid JSON");
- }
- // ask again ...
- }
- const deltaParser = new DeltaParser(deltaRegistry);
- const versionParser = new VersionParser(deltaParser, versionRegistry);
- versionParser.load(parsed, delta => {
- appendDelta(delta);
- }, version => {
- appendVersions([version]);
- });
- }}
- >Import</Mantine.Button>
- <Mantine.Tooltip label="Copied to clipboard!" opened={showTooltip !== null} withArrow>
- <Mantine.Button compact leftIcon={<Icons.IconDatabaseExport/>} disabled={inputs.length!==1} onClick={() => {
- const type = "application/json";
- const blob = new Blob([], {type});
- console.log(inputs[0].serialize());
- navigator.clipboard.writeText(
- JSON.stringify(inputs[0].serialize(), null, 2)
- );
- if (showTooltip !== null) clearTimeout(showTooltip);
- setShowTooltip(setTimeout(()=>setShowTooltip(null), 1500));
- }}>Export</Mantine.Button>
- </Mantine.Tooltip>
- </Mantine.Group>
- </>
- }
|