blob: ee7157dfb196852e6488c2a09e1a5ca42c536912 [file] [log] [blame]
import React, { FC, useState } from "react";
import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued";
import Highlight, { defaultProps } from "prism-react-renderer";
import { MdOutlineExpand } from "react-icons/md";
import { TbDelta } from "react-icons/tb";
import { AiOutlineCode, AiOutlineGithub } from "react-icons/ai";
import dracula from "prism-react-renderer/themes/vsLight";
import { BiNetworkChart } from "react-icons/bi";
import { FunctionGraphView } from "./FunctionGraphView";
import { NodeView } from "./CodeViewUtils";
import {
CodeArtifact,
CodeVersionGit1,
DAGTemplateWithData,
NodeTemplate,
getCodeVersion,
} from "../../../state/api/friendlyApi";
interface FunctionProps {
contents: string | undefined;
compareToContents: string | undefined;
codeArtifact: CodeArtifact;
compareCodeArtifact: CodeArtifact | undefined;
// codeArtifact: CodeArtifact;
// compareFn: HamiltonFunction | undefined;
upstreamNodes: NodeTemplate[];
nodesProducedByFunction: NodeTemplate[];
expanded: boolean;
toggleExpanded: (all: boolean) => void;
isHighlighted: boolean;
dagTemplate: DAGTemplateWithData; // Shouldn't be necessary but I don't want to refactor viz and I want to reuse it
}
const getCodeArtifactLink = (
codeArtifact: CodeArtifact,
dagTemplate: DAGTemplateWithData
) => {
/**
* Gets the link to the function in the git repo
* TODO -- move this to the server side. We'll want to have:
* (1) branch, if applicable
* (2) commit hash, in case the branch changes
* (3) generated perma-link to the function
*/
const functionPath = codeArtifact.path;
// TODO -- verify that this works
const gitVersion = getCodeVersion<CodeVersionGit1>(
dagTemplate,
"CodeVersionGit1"
);
if (gitVersion === undefined || gitVersion.git_repo.startsWith("Error:")) {
return undefined; // TODO -- add more support
}
const out = `https://${
gitVersion?.git_repo
.replace("https://", "")
.replace("git@", "")
.replace(":", "/")
.replace(".git", "") // TODO -- re-parse this correctly
}/blob/${gitVersion?.git_hash}/${functionPath}#L${codeArtifact.start}-L${
codeArtifact.end - 1
}`;
return out;
};
export const CodeView: React.FC<{
fnContents: string;
displayFnExpand?: boolean;
}> = (props) => {
const [expanded, setExpanded] = useState(true);
return (
<div className={`text-sm w-full`}>
{/* <h1>{fn.name}</h1> */}
<div className="flex justify-end relative">
{props.displayFnExpand && (
<MdOutlineExpand
className="text-lg hover:scale-125"
onClick={() => setExpanded(!expanded)}
/>
)}
</div>
{expanded && (
<Highlight
{...defaultProps}
theme={dracula}
code={props.fnContents}
language="python"
>
{({ className, style, tokens, getLineProps, getTokenProps }) => {
const styleToRender = {
...style,
backgroundColor: "transparent",
"word-break": "break-all",
"white-space": "pre-wrap",
};
className += "";
return (
<pre className={className} style={styleToRender}>
{tokens.map((line, i) => (
// eslint-disable-next-line react/jsx-key
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
// eslint-disable-next-line react/jsx-key
<span hidden={false} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
);
}}
</Highlight>
)}
</div>
);
};
export const DiffView = (props: {
fnContents: string;
compareFnContents: string;
}) => (
<ReactDiffViewer
// leftTitle={props.codeTitle}
// rightTitle={props.priorCodeTitle}
compareMethod={DiffMethod.LINES}
oldValue={props.fnContents}
newValue={props.compareFnContents}
splitView={false} // <div>
styles={{ line: { wordBreak: "break-all" } }}
/>
);
const NodeListView: React.FC<{
nodes: NodeTemplate[];
represents: "input" | "output";
}> = ({ nodes, represents }) => {
return (
<>
{nodes.map((node, index) => (
<NodeView key={index} node={node} type={represents}></NodeView>
))}
</>
);
};
const NodeInputOutputView: React.FC<{
consumes: NodeTemplate[];
produces: NodeTemplate[];
}> = ({ consumes, produces }) => {
return (
<div className="flex flex-col gap-1 py-2 cursor-default">
<div className="flex flex-row flex-wrap gap-1 pr-1 items-center">
<span className="text-gray-500 text-sm">input</span>
<NodeListView nodes={consumes} represents="input" />
</div>
<div className="flex flex-row flex-wrap gap-1 pr-1 items-center">
<span className="text-gray-500 text-sm">output</span>
<NodeListView nodes={produces} represents="output" />
</div>
</div>
);
};
const FunctionView: FC<FunctionProps> = ({
codeArtifact,
compareToContents,
nodesProducedByFunction,
upstreamNodes,
expanded,
toggleExpanded,
contents,
isHighlighted,
dagTemplate,
}) => {
/**
* Basic function view -- allows for linking out, visualizing the DAG, etc...
*/
// Probably don't need two but I'll mess with it
const DisplayIcon = expanded ? MdOutlineExpand : MdOutlineExpand;
const CodeLinkIcon = AiOutlineGithub;
const GraphViewIcon = BiNetworkChart;
const DiffViewIcon = TbDelta;
const CodeViewIcon = AiOutlineCode;
const functionLink = getCodeArtifactLink(codeArtifact, dagTemplate);
const diffModeAvailable = compareToContents !== undefined;
const handleExpand = (e: React.MouseEvent) => {
e.stopPropagation();
if (e.altKey) {
toggleExpanded(true);
} else {
toggleExpanded(false);
}
};
type ViewState = "graph" | "nodes" | "hidden";
type DisplayMode = "code" | "diff";
const [displayMode, setDisplayMode] = React.useState<DisplayMode>("code");
const [miniGraphView, setMiniGraphView] = React.useState<ViewState>("nodes");
const toggleMiniGraphView = () => {
const views: ViewState[] = ["nodes", "graph", "hidden"];
const indexView = views.indexOf(miniGraphView);
const nextView = views[(indexView + 1) % views.length];
setMiniGraphView(nextView);
};
return (
<div
className={`flex flex-col gap-2 px-2 rounded-md shadow border border-gray-200 ${
isHighlighted ? "bg-dwlightblue/10" : "bg-white"
}`}
>
<div>
<div
className={`border-b border-gray-100 py-2 flex flex-row justify-between items-center relative top-0 z-0`}
>
<h3 className="font-medium leading-6 text-gray-900">
{codeArtifact.name}
</h3>
<div className="flex flex-row text-gray-500 gap-2 text-lg z-0">
{diffModeAvailable ? (
displayMode === "code" ? (
<DiffViewIcon
className="hover:scale-125 hover:cursor-pointer"
onClick={() => setDisplayMode("diff")}
/>
) : (
<CodeViewIcon
className="hover:scale-125 hover:cursor-pointer"
onClick={() => setDisplayMode("code")}
/>
)
) : (
<></>
)}
<a
href={functionLink}
target="_blank"
rel="noreferrer"
className={
functionLink === undefined
? "pointer-events-none cursor-default"
: ""
}
>
<CodeLinkIcon
className={
functionLink === undefined
? "opacity-30"
: "hover:scale-125 hover:cursor-pointer"
}
/>
</a>
<DisplayIcon
onClick={handleExpand}
className="hover:scale-125 hover:cursor-pointer"
/>
<GraphViewIcon
className="hover:scale-125 hover:cursor-pointer"
onClick={toggleMiniGraphView}
/>
</div>
</div>
</div>
{expanded && contents ? ( // TODO -- display a dummy if we don't have code...
displayMode === "code" || !diffModeAvailable ? (
<CodeView fnContents={contents} />
) : (
<DiffView
fnContents={contents}
compareFnContents={compareToContents}
/>
)
) : (
<div className="flex items-center justify-center h-full text-gray-400 italic font-code">
No code recorded...
</div>
)}
<div
className={`w-full ${
miniGraphView != "hidden" && "border-t"
} border-gray-200`}
>
{miniGraphView === "graph" && (
<FunctionGraphView
upstreamNodes={upstreamNodes}
nodesProducedByFunction={nodesProducedByFunction}
dagTemplate={dagTemplate}
/>
)}
{miniGraphView == "nodes" && (
<NodeInputOutputView
consumes={[...upstreamNodes]}
produces={[...nodesProducedByFunction]}
/>
)}
</div>
</div>
);
};
export default FunctionView;