Skip to content

Commit

Permalink
Fix #8. Separate extension UI into Analyze and Visualize tabs. (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
parkerziegler committed Jun 16, 2023
1 parent 2fe4fa8 commit e2a685c
Show file tree
Hide file tree
Showing 18 changed files with 950 additions and 44 deletions.
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "reviz",
"description": "Adds tools for reverse engineering data visualizations to Chrome Developer tools.",
"version": "0.4.0",
"version": "0.4.1",
"devtools_page": "devtools.html",
"action": {
"default_icon": "reviz.png"
Expand Down
6 changes: 6 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@
"format": "prettier --write ."
},
"dependencies": {
"@codemirror/lang-javascript": "^6.1.9",
"@observablehq/plot": "^0.6.8",
"@plait-lab/reviz": "^0.4.1",
"@radix-ui/react-tabs": "^1.0.4",
"classnames": "^2.3.2",
"codemirror": "^6.0.1",
"d3-dsv": "^3.0.1",
"prettier": "^2.8.8",
"prism-react-renderer": "^2.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/chrome": "^0.0.237",
"@types/d3-dsv": "^3.0.1",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
Expand Down
19 changes: 17 additions & 2 deletions extension/scripts/inspect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { analyzeVisualization } from "@plait-lab/reviz";
import { RevizOutput, analyzeVisualization } from "@plait-lab/reviz";

// The class name to apply to an element when hovered.
const MOUSE_VISITED_CLASSNAME = "mouse-visited";
Expand Down Expand Up @@ -27,6 +27,11 @@ function onMouseLeave(event: MouseEvent) {
el.classList.remove(MOUSE_VISITED_CLASSNAME);
}

export interface AnalyzedVisualization extends RevizOutput {
nodeName: string;
classNames: string;
}

/**
* Analyzes a visualization on click and sends the result to the extension
* service worker. The element cast is safe because we only register this event
Expand All @@ -36,7 +41,17 @@ function onMouseLeave(event: MouseEvent) {
*/
function onClick(event: MouseEvent) {
const el = event.target as SVGSVGElement;
chrome.runtime.sendMessage(analyzeVisualization(el));

const nodeName = el.nodeName;
const classNames = el.getAttribute("class") ?? "";
const { spec, program } = analyzeVisualization(el);

chrome.runtime.sendMessage<AnalyzedVisualization>({
nodeName,
classNames,
spec,
program,
});
}

/**
Expand Down
72 changes: 60 additions & 12 deletions extension/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import * as React from "react";
import type { RevizOutput } from "@plait-lab/reviz";
import * as Tabs from "@radix-ui/react-tabs";
import type { DSVRowArray } from "d3-dsv";

import type { AnalyzedVisualization } from "../scripts/inspect";

import ElementSelect from "./components/ElementSelect";
import ExtensionErrorBoundary from "./components/ExtensionErrorBoundary";
import ProgramViewer from "./components/ProgramViewer";
import SpecViewer from "./components/SpecViewer";
import DataPanel from "./components/data/DataPanel";
import ElementSelect from "./components/interaction/ElementSelect";
import ProgramEditor from "./components/program/ProgramEditor";
import ProgramOutput from "./components/program/ProgramOutput";
import ProgramViewer from "./components/program/ProgramViewer";
import SpecViewer from "./components/spec/SpecViewer";

export type VisualizationState = {
[Property in keyof AnalyzedVisualization]: Property extends "spec"
? AnalyzedVisualization[Property] | undefined
: AnalyzedVisualization[Property];
};

function App() {
const [revizOutput, setRevizOutput] = React.useState<RevizOutput | null>(
null
);
const [{ spec, program, nodeName, classNames }, setViz] =
React.useState<VisualizationState>({
spec: undefined,
program: "",
nodeName: "",
classNames: "",
});
const [data, setData] = React.useState<unknown | DSVRowArray>();

React.useEffect(() => {
// Establish a long-lived connection to the service worker.
Expand All @@ -24,18 +41,49 @@ function App() {

// Listen for messages from the content script sent via the service worker.
serviceWorkerConnection.onMessage.addListener((message) => {
setRevizOutput(message);
setViz(message);
});
}, []);

return (
<main className="absolute inset-0 grid grid-cols-12 grid-rows-[36px_minmax(0,_1fr)_minmax(0,_1fr)] bg-slate-900 text-white md:grid-rows-[36px_minmax(0,_1fr)]">
<main className="absolute inset-0 flex flex-col bg-slate-900 text-white">
<ExtensionErrorBoundary
fallback={(message) => <p>An error occurred. {message}</p>}
>
<ElementSelect />
<SpecViewer spec={revizOutput?.spec} />
<ProgramViewer code={revizOutput?.program} />
<Tabs.Root defaultValue="analyze" className="flex flex-1 flex-col">
<div className="flex border-b border-b-slate-500">
<ElementSelect nodeName={nodeName} classNames={classNames} />
<Tabs.List className="flex shrink-0">
<Tabs.Trigger
className="tab-trigger border-r border-slate-500 px-3 py-2 text-sm"
value="analyze"
>
Analyze
</Tabs.Trigger>
<Tabs.Trigger
className="tab-trigger px-3 py-2 text-sm"
value="visualize"
>
Visualize
</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.Content
value="analyze"
className="tab-content flex flex-1 flex-col lg:flex-row"
>
<SpecViewer spec={spec} />
<ProgramViewer program={program} />
</Tabs.Content>
<Tabs.Content
value="visualize"
className="tab-content flex flex-1 flex-col lg:flex-row"
>
<ProgramOutput program={program} data={data} />
<ProgramEditor program={program} />
<DataPanel data={data} setData={setData} />
</Tabs.Content>
</Tabs.Root>
</ExtensionErrorBoundary>
</main>
);
Expand Down
22 changes: 22 additions & 0 deletions extension/src/components/data/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { DSVRowArray } from "d3-dsv";

import Heading from "../shared/Heading";

import DataTable from "./DataTable";
import DataUpload from "./DataUpload";

interface Props {
data: unknown | DSVRowArray;
setData: (data: unknown | DSVRowArray) => void;
}

const DataPanel: React.FC<Props> = ({ data, setData }) => {
return (
<div className="stack stack-sm basis-1/3 px-3 py-2">
<Heading className="self-start">Data</Heading>
{data ? <DataTable data={data} /> : <DataUpload setData={setData} />}
</div>
);
};

export default DataPanel;
11 changes: 11 additions & 0 deletions extension/src/components/data/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DSVRowArray } from "d3-dsv";

interface Props {
data: unknown | DSVRowArray;
}

const DataTable: React.FC<Props> = () => {
return <p>We have some data!</p>;
};

export default DataTable;
81 changes: 81 additions & 0 deletions extension/src/components/data/DataUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from "react";
import { DSVRowArray, csvParse } from "d3-dsv";

interface Props {
setData: (data: unknown | DSVRowArray) => void;
}

const DataUpload: React.FC<Props> = ({ setData }) => {
const onChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files?.length) {
const type = event.target.files[0].type;
const reader = new FileReader();

reader.onload = (theFile) => {
if (
theFile.target?.result &&
typeof theFile.target.result === "string"
) {
switch (type) {
case "application/json":
setData(JSON.parse(theFile.target.result));
break;
case "text/csv":
setData(csvParse(theFile.target.result));
break;
}
}
};

reader.readAsText(event.target.files[0]);
}
},
[]
);

return (
<div className="flex h-full flex-col items-center justify-center border border-dashed border-slate-500">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p className="mt-2 text-sm">
Upload a{" "}
<label className="relative cursor-pointer border-b border-dotted border-primary text-primary">
<input
type="file"
accept=".csv, .json"
className="absolute h-full w-full opacity-[0.0001]"
onChange={onChange}
/>
CSV
</label>{" "}
or{" "}
<label className="relative cursor-pointer border-b border-dotted border-primary text-primary">
<input
type="file"
accept=".csv, .json"
className="absolute h-full w-full opacity-[0.0001]"
onChange={onChange}
/>
JSON
</label>{" "}
file.
</p>
</div>
);
};

export default DataUpload;
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from "react";
import cs from "classnames";

import type { VisualizationState } from "../../App";
import { formatClassNames } from "../../utils/formatters";

const MousePointer = (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -18,7 +21,9 @@ const MousePointer = (
</svg>
);

const ElementSelect: React.FC = () => {
type Props = Pick<VisualizationState, "nodeName" | "classNames">;

const ElementSelect: React.FC<Props> = ({ nodeName, classNames }) => {
const [isElementSelectActive, setElementSelectActive] = React.useState(false);

function toggleElementSelectActive() {
Expand All @@ -38,7 +43,7 @@ const ElementSelect: React.FC = () => {
}

return (
<div className="col-span-12 flex self-start border-b border-b-slate-500">
<div className="flex flex-1 overflow-hidden border-r border-slate-500">
<button
onClick={toggleElementSelectActive}
className={cs(
Expand All @@ -48,11 +53,22 @@ const ElementSelect: React.FC = () => {
>
{MousePointer}
</button>
<p className="px-3 py-2">
Select an{" "}
<code className="rounded bg-blue-50 px-1 py-0.5 text-primary">svg</code>{" "}
element to inspect.
</p>
{nodeName ? (
<p className="flex truncate px-3 py-2 text-primary">
<code className="truncate rounded bg-blue-50 px-1 py-0.5">
<span className="text-secondary">{nodeName}</span>
{classNames ? <span>{formatClassNames(classNames)}</span> : null}
</code>
</p>
) : (
<p className="px-3 py-2">
Select an{" "}
<code className="rounded bg-blue-50 px-1 py-0.5 text-primary">
svg
</code>{" "}
element to inspect.
</p>
)}
</div>
);
};
Expand Down
42 changes: 42 additions & 0 deletions extension/src/components/program/ProgramEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from "react";
import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";

import Heading from "../shared/Heading";
import { formatProgram } from "../../utils/formatters";

interface Props {
program: string;
}

const ProgramEditor: React.FC<Props> = ({ program }) => {
const editorRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
const editor = new EditorView({
extensions: [basicSetup, javascript()],
parent: editorRef.current!,
doc: formatProgram(program),
});

return () => {
editor.destroy();
};
}, [program]);

return (
<div className="stack stack-sm relative basis-1/3 border-b border-slate-500 px-3 py-2 lg:border-b-0 lg:border-r">
<Heading className="self-start">Program</Heading>
{program ? (
<div
ref={editorRef}
className="-mx-3 h-full bg-white text-xs text-black"
/>
) : (
<p>Waiting for visualization selection...</p>
)}
</div>
);
};

export default ProgramEditor;
Loading

1 comment on commit e2a685c

@vercel
Copy link

@vercel vercel bot commented on e2a685c Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

reviz – ./

reviz-git-main-parkerziegler.vercel.app
reviz-parkerziegler.vercel.app
reviz.vercel.app

Please sign in to comment.