Skip to content

Commit

Permalink
feat: improve ui
Browse files Browse the repository at this point in the history
  • Loading branch information
DerYeger committed Aug 16, 2023
1 parent eb86335 commit c2e4b04
Show file tree
Hide file tree
Showing 27 changed files with 1,266 additions and 401 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-foxes-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@yeger/debounce': minor
---

add optional args to callback
9 changes: 6 additions & 3 deletions packages/debounce/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
* @param delay - A delay after which the callback will be invoked.
* @returns The debounced callback.
*/
export function debounce(cb: () => void, delay?: number) {
export function debounce<Args extends any[]>(
cb: (...args: Args) => void,
delay?: number,
) {
let timeout: any
return () => {
return (...args: Args) => {
if (timeout !== undefined) {
clearTimeout(timeout)
}
timeout = setTimeout(() => cb(), delay)
timeout = setTimeout(() => cb(...args), delay)
}
}
6 changes: 3 additions & 3 deletions packages/streams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"require": "./dist/index.umd.js",
"import": "./dist/index.mjs"
"import": "./dist/index.mjs",
"require": "./dist/index.umd.js"
}
},
"main": "dist/index.umd.js",
Expand All @@ -38,7 +38,7 @@
"devDependencies": {
"@yeger/tsconfig": "workspace:*",
"typescript": "5.1.6",
"vite": "4.4.4",
"vite": "4.4.9",
"vite-plugin-lib": "workspace:*"
},
"publishConfig": {
Expand Down
7 changes: 5 additions & 2 deletions packages/streams/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ export abstract class Stream<T> implements Iterable<T> {
return new Map(stream)
}

public toRecord(fn: Processor<T, string>): Record<string, T> {
return Object.fromEntries(this.map((x) => [fn(x), x] as const))
public toRecord<U>(
key: Processor<T, string>,
value: Processor<T, U>,
): Record<string, U> {
return Object.fromEntries(this.map((x) => [key(x), value?.(x)] as const))
}

public abstract [Symbol.iterator](): IterableIterator<T>
Expand Down
1 change: 1 addition & 0 deletions packages/tsconfig/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"compilerOptions": {
"declaration": false,
"module": "CommonJS",
"noEmit": false,
"sourceMap": false
}
}
3 changes: 2 additions & 1 deletion packages/tsconfig/next.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./web.json",
"compilerOptions": {
"noEmit": true,
"incremental": true
"incremental": true,
"plugins": [{ "name": "next" }]
}
}
80 changes: 80 additions & 0 deletions packages/turbo-graph-ui/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;

--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;

--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;

--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;

--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;

--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;

--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;

--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;

--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;

--radius: 0.5rem;
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;

--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;

--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;

--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;

--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;

--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;

--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;

--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;

--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: hsl(212.7, 26.8%, 83.9);
}
}

@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

.react-flow__edge {
pointer-events: none !important;
}
26 changes: 26 additions & 0 deletions packages/turbo-graph-ui/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FilterInput, TaskInput } from '../components/GraphInputs'
import './globals.css'

export default function RootLayout({
// Layouts must accept a children prop.
// This will be populated with nested layouts or pages
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="h-full w-full">
<body className="m-0 h-full w-full">
<div className="h-full w-full">
<main className="relative flex h-full w-full flex-col">
<div className="absolute inset-x-0 top-0 z-10 flex gap-2 border-gray-400 bg-none p-2 shadow-xl backdrop-blur-sm">
<TaskInput />
<FilterInput />
</div>
{children}
</main>
</div>
</body>
</html>
)
}
4 changes: 4 additions & 0 deletions packages/turbo-graph-ui/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <span>Loading</span>
}
33 changes: 33 additions & 0 deletions packages/turbo-graph-ui/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Metadata } from 'next'

import { Graph } from '../components/Graph'

export const metadata: Metadata = {
title: 'Turbo Graph',
// m
// <meta
// name="description"
// content="Interactive visualization of Turborepo task graphs."
// />
// <link rel="icon" href="/favicon.ico" />
// </Head>
}

// TODO: Add error boundary
// TODO: Add loading state
export default async function Home({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined }
}) {
const rawTasks = searchParams?.tasks
const tasks = Array.isArray(rawTasks)
? rawTasks
: rawTasks?.split(' ') ?? ['build']
const filter = searchParams?.filter
if (filter && Array.isArray(filter)) {
throw new Error(`Unsupported filter ${filter}`)
}

return <Graph tasks={tasks} filter={filter} />
}
132 changes: 132 additions & 0 deletions packages/turbo-graph-ui/components/FlowGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use client'

import { Stream } from '@yeger/streams'
import { scaleOrdinal } from 'd3-scale'
import { schemeSet3 } from 'd3-scale-chromatic'
import { useEffect, useMemo } from 'react'
import {
Background,
Controls,
Handle,
MiniMap,
Position,
ReactFlow,
useReactFlow,
} from 'reactflow'

import 'reactflow/dist/style.css'
import {
type FlowNode,
TASK_HEIGHT_VAR,
TASK_WIDTH_VAR,
convertGraph,
getTaskColorVar,
} from '../lib/flow'
import type { TurboGraph } from '../lib/turbo'
import { useGraphSettings } from '../lib/utils'

export interface Props {
graph: TurboGraph
uniqueTasks: Set<string>
}

interface TaskProps {
data: FlowNode
}

function Task({ data }: TaskProps) {
const { task, workspace, isTerminal, isOrigin } = data
return (
<div className="flex flex-col">
{isOrigin ? null : (
<Handle type="target" position={Position.Top} isConnectable={false} />
)}
<div
className="flex flex-col rounded p-2 outline outline-2 outline-neutral-700"
style={{
width: `var(${TASK_WIDTH_VAR})`,
height: `var(${TASK_HEIGHT_VAR})`,
backgroundColor: `var(${getTaskColorVar(task)})`,
}}
>
<div className="font-bold">{task}</div>
<div className="text-right text-neutral-800">{workspace}</div>
</div>
{isTerminal ? null : (
<Handle
type="source"
position={Position.Bottom}
isConnectable={false}
/>
)}
</div>
)
}

const nodeTypes = {
task: Task,
}

export function FlowGraph({ graph, uniqueTasks }: Props) {
const { flowGraph, taskCssVars } = useMemo(() => {
const flowGraph = convertGraph(graph)
const getColor = scaleOrdinal(schemeSet3).domain(uniqueTasks)
const taskColors = Stream.from(uniqueTasks).toRecord(
(task) => getTaskColorVar(task),
(task) => getColor(task),
)
const taskSize: Record<string, string> = {
[TASK_WIDTH_VAR]: `${flowGraph.sizeConfig.width}px`,
[TASK_HEIGHT_VAR]: `${flowGraph.sizeConfig.height}px`,
}
const taskCssVars = { ...taskColors, ...taskSize }
return {
flowGraph,
taskColors,
taskCssVars,
}
}, [graph, uniqueTasks])

const { setParameter } = useGraphSettings()

const onNodeClicked = (_: unknown, { data }: { data: FlowNode }) => {
setParameter('filter', data.workspace)
}

return (
<ReactFlow
nodes={flowGraph.nodes}
edges={flowGraph.edges}
nodeTypes={nodeTypes}
minZoom={0.1}
style={taskCssVars}
edgesFocusable={false}
edgesUpdatable={false}
nodesDraggable={false}
nodesConnectable={false}
onNodeContextMenu={onNodeClicked}
onNodeDoubleClick={onNodeClicked}
zoomOnDoubleClick={false}
>
<Background />
<Controls showInteractive={false} />
<MiniMap
nodeColor={({ data }: { data: FlowNode }) =>
`var(${getTaskColorVar(data.task)})`
}
nodeStrokeColor="#000000"
/>
<ViewFitter graph={graph} />
</ReactFlow>
)
}

function ViewFitter({ graph }: { graph: TurboGraph }) {
const reactFlow = useReactFlow()
useEffect(() => {
setTimeout(() => {
reactFlow.fitView({ duration: 200, padding: 0.3 })
}, 200)
}, [reactFlow, graph])
return null
}
38 changes: 38 additions & 0 deletions packages/turbo-graph-ui/components/Graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Stream } from '@yeger/streams'

import { getGraph } from '../lib/turbo'

import { FlowGraph } from './FlowGraph'

export interface GraphProps {
tasks: string[]
filter?: string
}

// TODO: Add error boundary
// TODO: Add loading state
export async function Graph({ tasks, filter }: GraphProps) {
const graphResult = await getGraph(tasks, filter)

if (graphResult.isError) {
// TODO: Improve error message
return (
<div className="flex h-full w-full items-center justify-center p-4">
<code className="text-justify text-sm text-red-500">
{graphResult.getError().message}
</code>
</div>
)
}

const graph = graphResult.get()
const uniqueTasks = Stream.from(graph.nodes)
.map(({ task }) => task)
.toSet()

return (
<div className="h-full w-full">
<FlowGraph graph={graph} uniqueTasks={uniqueTasks} />
</div>
)
}
Loading

0 comments on commit c2e4b04

Please sign in to comment.