[playground] refactor output tabs

Removes a bit of indirection and opens the way to more easily customize specific 
tabs.
This commit is contained in:
Jan Kassens
2022-11-10 14:32:40 -05:00
parent 21b6112238
commit 5e86e6a9c8
3 changed files with 120 additions and 144 deletions
@@ -1,24 +1,21 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import TabbedWindow from "../TabbedWindow";
import MonacoEditor from "@monaco-editor/react";
import {
CompilerOutputs,
OutputKind,
stringifyCompilerOutputs,
type Diagnostic,
} from "babel-plugin-react-forget";
import clsx from "clsx";
import { memo, useEffect, useMemo, useState } from "react";
import { memo, useEffect, useMemo } from "react";
import compile from "../../lib/compilerDriver";
import {
getSelectedFile,
getVisualizedGraphKind,
isGraph,
isVisualizedGraph,
type OutputTabKind,
type Store,
} from "../../lib/stores";
import { getSelectedFile, type Store } from "../../lib/stores";
import GraphView from "../GraphView";
import Preview from "../Preview";
import { monacoOptions } from "./monacoOptions";
@@ -33,118 +30,74 @@ type Props = {
};
function Output({ store, updateDiagnostics }: Props) {
const [selectedTab, setSelectedTab] = useState<OutputTabKind>("Preview");
const { outputs, diagnostics } = useMemo(
() => compile(getSelectedFile(store), store.compilerFlags),
[store]
);
useEffect(() => {
updateDiagnostics(diagnostics);
}, [diagnostics, updateDiagnostics]);
return (
<TabbedWindow
defaultTab="HIR"
tabs={{
Preview: <Preview store={store} />,
IR: <TextTabContent outputs={outputs} kind={OutputKind.IR} />,
HIR: <TextTabContent outputs={outputs} kind={OutputKind.HIR} />,
CFG: <TextTabContent outputs={outputs} kind={OutputKind.CFG} />,
ValGraph: (
<GraphTabContent outputs={outputs} kind={OutputKind.ValGraph} />
),
SCCGraph: (
<GraphTabContent outputs={outputs} kind={OutputKind.SCCGraph} />
),
RedGraph: (
<GraphTabContent outputs={outputs} kind={OutputKind.RedGraph} />
),
LIR: <TextTabContent outputs={outputs} kind={OutputKind.LIR} />,
JS: <TextTabContent outputs={outputs} kind={OutputKind.JS} />,
}}
/>
);
}
function TextTabContent({
outputs,
kind,
}: {
outputs: CompilerOutputs;
kind: OutputKind;
}) {
const prettyOutputs = useMemo(
() => stringifyCompilerOutputs(outputs),
[outputs]
);
const outputTabs = [
["Preview", "Preview"] as const,
...Object.entries(OutputKind),
];
useEffect(() => {
updateDiagnostics(diagnostics);
}, [diagnostics, updateDiagnostics]);
const outputContainer = () => {
// Special-case Preview to keep it mounted, saving its state.
if (selectedTab === "Preview") return null;
if (isVisualizedGraph(selectedTab)) {
const graphKind = getVisualizedGraphKind(selectedTab);
return (
<div className="w-full h-full overflow-auto">
<GraphView graphs={outputs[graphKind]} />
</div>
);
}
return (
<div className="w-full h-full">
<MonacoEditor
path={selectedTab}
defaultLanguage="javascript"
value={prettyOutputs[selectedTab] ?? "(Empty)"}
options={{
...monacoOptions,
readOnly: true,
}}
/>
</div>
);
};
// TODO: Abstract into "TabbedWindow" component to reuse across input/output.
// TODO: Add indicator that output tabs have overflowed as part of responsive design.
return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-none overflow-x-auto border-b border-gray-200 h-9 no-scrollbar">
{outputTabs.map(([outputName, outputKind], index) => {
const isGraphViewSelected = selectedTab === `Visualized${outputKind}`;
const isTextViewSelected = selectedTab === outputKind;
const shouldDivide = index < outputTabs.length - 1;
<div className="w-full h-full">
<MonacoEditor
path={kind}
defaultLanguage="javascript"
value={prettyOutputs[kind] ?? "(Empty)"}
options={{
...monacoOptions,
readOnly: true,
}}
/>
</div>
);
}
return (
<div className="flex items-center h-full" key={outputName}>
<div
className={clsx(
"h-full border-b-2 border-white px-4 py-0.5 flex items-center",
{
"border-link": isTextViewSelected || isGraphViewSelected,
}
)}
>
<button
disabled={isTextViewSelected}
onClick={() => setSelectedTab(outputKind)}
className="transition-colors duration-150 ease-in"
>
{outputName}
</button>
{isGraph(outputKind) ? (
<button
title={`Visualize ${outputName}`}
onClick={() =>
setSelectedTab(
isGraphViewSelected
? outputKind
: `Visualized${outputKind}`
)
}
className="flex-none pl-2 group"
>
<div
title={`Show visualized ${outputName}`}
className={clsx(
"px-1 text-sm font-medium tracking-wide uppercase rounded text-link bg-highlight whitespace-nowrap",
{
"bg-link text-highlight": isGraphViewSelected,
}
)}
>
Vis
</div>
</button>
) : null}
</div>
{shouldDivide && (
<div className="w-[1px] h-[18px] border-gray-200 border-l mx-1" />
)}
</div>
);
})}
</div>
<div
className={clsx("w-full h-full bg-card", {
hidden: selectedTab !== "Preview",
})}
>
<Preview store={store} />
</div>
{outputContainer()}
function GraphTabContent({
outputs,
kind,
}: {
outputs: CompilerOutputs;
kind: OutputKind.ValGraph | OutputKind.SCCGraph | OutputKind.RedGraph;
}) {
return (
<div className="w-full h-full overflow-auto">
<GraphView graphs={outputs[kind]} />
</div>
);
}
@@ -0,0 +1,54 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import clsx from "clsx";
import React, { useState } from "react";
export default function TabbedWindow(props: {
defaultTab: string | null;
tabs: { [name: string]: React.ReactNode };
}): React.ReactElement {
let [selected, setSelected] = useState<string | null>(props.defaultTab);
if (selected === null) {
selected = Object.keys(props.tabs)[0];
}
return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-none overflow-x-auto border-b border-gray-200 h-9 no-scrollbar">
{Object.keys(props.tabs).map((name, index, all) => {
const isFirst = index === 0;
const isSelected = name === selected;
return (
<React.Fragment key={name}>
{isFirst ? null : (
<div className="w-[1px] h-[18px] self-center border-gray-200 border-l mx-1" />
)}
<div
className={clsx(
"h-full border-b-2 border-white px-4 py-0.5 flex items-center",
{
"border-link": isSelected,
}
)}
>
<button
disabled={isSelected}
onClick={() => setSelected(name)}
className="transition-colors duration-150 ease-in"
>
{name}
</button>
</div>
</React.Fragment>
);
})}
</div>
{props.tabs[selected]}
</div>
);
}
@@ -9,37 +9,6 @@ import { codec } from "../utils";
import { ForgetCompilerFlags } from "../compilerDriver";
import { parseCompilerFlags } from "babel-plugin-react-forget";
type VisualizedGraph =
| `Visualized${OutputKind.ValGraph}`
| `Visualized${OutputKind.SCCGraph}`
| `Visualized${OutputKind.RedGraph}`;
export type OutputTabKind = OutputKind | VisualizedGraph | "Preview";
export function isGraph(
tabKind: OutputTabKind
): tabKind is
| OutputKind.ValGraph
| OutputKind.SCCGraph
| OutputKind.RedGraph
| VisualizedGraph {
return tabKind.endsWith("Graph");
}
export function isVisualizedGraph(
tabKind: OutputTabKind
): tabKind is VisualizedGraph {
return tabKind.startsWith("Visualized");
}
export function getVisualizedGraphKind(graphKind: VisualizedGraph) {
const kind = graphKind.slice("Visualized".length);
return kind as
| OutputKind.ValGraph
| OutputKind.SCCGraph
| OutputKind.RedGraph;
}
export enum FileExt {
js = "javascript",
jsx = "javascript",