Skip to content

Commit cb776e1

Browse files
authored
Tree routes (#46)
* Tree view of route tab * v2.3.0 - Tree route view
1 parent c19b5a2 commit cb776e1

File tree

10 files changed

+846
-115
lines changed

10 files changed

+846
-115
lines changed

package-lock.json

+498
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "remix-development-tools",
33
"description": "Remix development tools.",
44
"author": "Alem Tuzlak",
5-
"version": "2.2.0",
5+
"version": "2.3.0",
66
"license": "MIT",
77
"keywords": [
88
"remix",
@@ -108,6 +108,7 @@
108108
"@uiw/react-json-view": "^1.8.4",
109109
"clsx": "^2.0.0",
110110
"lucide-react": "^0.263.1",
111+
"react-d3-tree": "^3.6.1",
111112
"react-use-websocket": "^4.3.1",
112113
"tailwind-merge": "^1.14.0"
113114
}
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import clsx from "clsx";
2+
import { Input } from "./Input";
3+
import { useSettingsContext } from "../context/useRDTContext";
4+
import { ExtendedRoute, constructRoutePath } from "../utils/routing";
5+
import type { MouseEvent } from "react";
6+
import { Tag } from "./Tag";
7+
import { X } from "lucide-react";
8+
9+
interface RouteInfoProps {
10+
route: ExtendedRoute;
11+
className?: string;
12+
openNewRoute: (path: string) => (e?: MouseEvent<HTMLDivElement | HTMLButtonElement>) => void;
13+
onClose?: () => void;
14+
}
15+
16+
export const RouteInfo = ({ route, className, openNewRoute, onClose }: RouteInfoProps) => {
17+
const { settings, setSettings } = useSettingsContext();
18+
const { routeWildcards, routeViewMode } = settings;
19+
const { hasWildcard, path, pathToOpen } = constructRoutePath(route, routeWildcards);
20+
const isTreeView = routeViewMode === "tree";
21+
return (
22+
<div className={clsx(className, "rdt-relative")}>
23+
{isTreeView && (
24+
<>
25+
<X onClick={onClose} className="rdt-absolute rdt-right-2 rdt-top-2 rdt-cursor-pointer rdt-text-red-600" />
26+
<h1 className="rdt-text-xl rdt-font-semibold">{route.url}</h1>
27+
<hr className="rdt-mb-4 rdt-mt-1" />
28+
<h3>
29+
<span className="rdt-text-gray-500">Path:</span> {path}
30+
</h3>
31+
<h3>
32+
<span className="rdt-text-gray-500">Url:</span> {pathToOpen}
33+
</h3>
34+
</>
35+
)}
36+
<div className="rdt-flex rdt-gap-2">
37+
<span className="rdt-text-gray-500">Key:</span>
38+
{route.id}
39+
</div>
40+
<div className="rdt-mb-4 rdt-mt-4 rdt-flex rdt-flex-col rdt-gap-2">
41+
<span className="rdt-text-gray-500">Components contained in the route:</span>
42+
<div className="rdt-flex rdt-gap-2">
43+
<Tag color={route.hasLoader ? "GREEN" : "RED"}>Loader</Tag>
44+
<Tag color={route.hasAction ? "GREEN" : "RED"}>Action</Tag>
45+
<Tag color={route.hasErrorBoundary ? "GREEN" : "RED"}>ErrorBoundary</Tag>
46+
</div>
47+
</div>
48+
{hasWildcard && (
49+
<>
50+
<p className="rdt-mb-2 rdt-text-gray-500">Wildcard parameters:</p>
51+
<div
52+
className={clsx("rdt-mb-4 rdt-grid rdt-w-full rdt-grid-cols-2 rdt-gap-2", isTreeView && "rdt-grid-cols-1")}
53+
>
54+
{route.url
55+
.split("/")
56+
.filter((p) => p.startsWith(":"))
57+
.map((param) => (
58+
<div key={param} className="rdt-flex rdt-w-full rdt-gap-2">
59+
<Tag key={param} color="BLUE">
60+
{param}
61+
</Tag>
62+
<Input
63+
value={routeWildcards[route.id]?.[param] || ""}
64+
onChange={(e) =>
65+
setSettings({
66+
routeWildcards: {
67+
...routeWildcards,
68+
[route.id]: {
69+
...routeWildcards[route.id],
70+
[param]: e.target.value,
71+
},
72+
},
73+
})
74+
}
75+
placeholder={param}
76+
/>
77+
</div>
78+
))}
79+
</div>
80+
</>
81+
)}
82+
{isTreeView && (
83+
<button
84+
className="rdt-mr-2 rdt-whitespace-nowrap rdt-rounded rdt-border rdt-border-gray-400 rdt-px-2 rdt-py-1 rdt-text-sm"
85+
onClick={openNewRoute(path)}
86+
>
87+
Open in browser
88+
</button>
89+
)}
90+
</div>
91+
);
92+
};
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import clsx from "clsx";
2+
import { CustomNodeElementProps } from "react-d3-tree";
3+
import { RouteWildcards } from "../context/rdtReducer";
4+
import { ExtendedRoute, getRouteColor } from "../utils/routing";
5+
6+
export const RouteNode = ({
7+
nodeDatum,
8+
hierarchyPointNode,
9+
toggleNode,
10+
setActiveRoute,
11+
activeRoutes,
12+
}: CustomNodeElementProps & {
13+
routeWildcards: RouteWildcards;
14+
setActiveRoute: (e: ExtendedRoute) => void;
15+
activeRoutes: string[];
16+
}) => {
17+
const parent = hierarchyPointNode.parent?.data;
18+
const parentName = parent && parent?.name !== "/" ? parent.name : "";
19+
const name = nodeDatum.name.replace(parentName, "") ?? "/";
20+
const route = nodeDatum.attributes as any as ExtendedRoute;
21+
return (
22+
<g className="rdt-flex">
23+
<circle
24+
x={20}
25+
onClick={toggleNode}
26+
className={clsx(
27+
getRouteColor(route),
28+
"rdt-stroke-white",
29+
nodeDatum.__rd3t.collapsed && nodeDatum.children?.length && "rdt-fill-gray-800"
30+
)}
31+
r={12}
32+
></circle>
33+
<g>
34+
<foreignObject y={-15} x={17} width={110} height={140}>
35+
<text
36+
onClick={() => setActiveRoute(route)}
37+
style={{ width: 100, fontSize: 14 }}
38+
className={clsx(
39+
"rdt-w-full rdt-break-all rdt-fill-white rdt-stroke-transparent",
40+
activeRoutes.includes(route.id) && "rdt-text-yellow-500"
41+
)}
42+
>
43+
{nodeDatum.attributes?.id === "root" ? "Root" : name ? name : "Index"}
44+
</text>
45+
</foreignObject>
46+
</g>
47+
</g>
48+
);
49+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import clsx from "clsx";
2+
import { Network, List } from "lucide-react";
3+
import { useSettingsContext } from "../context/useRDTContext";
4+
5+
export const RouteToggle = () => {
6+
const { settings, setSettings } = useSettingsContext();
7+
const { routeViewMode } = settings;
8+
return (
9+
<div className="rdt-absolute rdt-left-0 rdt-top-0 rdt-flex rdt-items-center rdt-gap-2 rdt-rounded-lg rdt-border rdt-border-white rdt-px-3 rdt-py-1">
10+
<Network
11+
className={clsx("hover:rdt-cursor-pointer", routeViewMode === "tree" && "rdt-stroke-yellow-500")}
12+
onClick={() => setSettings({ routeViewMode: "tree" })}
13+
size={20}
14+
/>
15+
/
16+
<List
17+
className={clsx("hover:rdt-cursor-pointer", routeViewMode === "list" && "rdt-stroke-yellow-500")}
18+
onClick={() => setSettings({ routeViewMode: "list" })}
19+
size={20}
20+
/>
21+
</div>
22+
);
23+
};

src/RemixDevTools/context/rdtReducer.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type RemixDevToolsState = {
3939
expansionLevel: number;
4040
hoveredRoute: string;
4141
isHoveringRoute: boolean;
42+
routeViewMode: "list" | "tree";
4243
};
4344
persistOpen: boolean;
4445
detachedWindow: boolean;
@@ -63,6 +64,7 @@ export const initialState: RemixDevToolsState = {
6364
expansionLevel: 0,
6465
hoveredRoute: "",
6566
isHoveringRoute: false,
67+
routeViewMode: "tree",
6668
},
6769
persistOpen: false,
6870
detachedWindow: false,

src/RemixDevTools/hooks/useTabs.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { useEffect, useMemo } from "react";
22
import { RemixDevToolsProps } from "../RemixDevTools";
33
import { useSettingsContext } from "../context/useRDTContext";
4-
import { tabs } from "../tabs";
4+
import { Tab, tabs } from "../tabs";
5+
import type { Tabs } from "../tabs";
6+
import { RemixDevToolsState } from "../context/rdtReducer";
7+
8+
const shouldHideTimeline = (activeTab: Tabs, tab: Tab | undefined, settings: RemixDevToolsState["settings"]) => {
9+
if (activeTab === "routes" && settings.routeViewMode === "tree") return true;
10+
return tab?.hideTimeline;
11+
};
512

613
export const useTabs = (isConnected: boolean, isConnecting: boolean, plugins?: RemixDevToolsProps["plugins"]) => {
714
const { settings, setSettings } = useSettingsContext();
815
const { activeTab } = settings;
916
const allTabs = useMemo(() => [...tabs, ...(plugins ? plugins : [])], [plugins]);
17+
1018
const { Component, hideTimeline } = useMemo(() => {
1119
const tab = allTabs.find((tab) => tab.id === activeTab);
12-
return { Component: tab?.component, hideTimeline: tab?.hideTimeline };
13-
}, [activeTab, allTabs]);
20+
return { Component: tab?.component, hideTimeline: shouldHideTimeline(activeTab, tab, settings) };
21+
}, [activeTab, allTabs, settings]);
1422
const visibleTabs = useMemo(
1523
() => allTabs.filter((tab) => !(!isConnected && tab.requiresForge)),
1624
[isConnected, allTabs]

0 commit comments

Comments
 (0)