Skip to content

Commit

Permalink
feat(dev-tools): improve support for non-store recs components (#1302)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Aug 15, 2023
1 parent 753bdce commit 5294a7d
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .changeset/smooth-pots-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@latticexyz/dev-tools": patch
"@latticexyz/store-sync": patch
---

Improves support for internal/client-only RECS components
9 changes: 7 additions & 2 deletions packages/dev-tools/src/recs/ComponentData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useParams } from "react-router-dom";
import { useDevToolsContext } from "../DevToolsContext";
import { ComponentDataTable } from "./ComponentDataTable";
import { isStoreComponent } from "@latticexyz/store-sync/recs";
import { StoreComponentDataTable } from "./StoreComponentDataTable";

// TODO: use react-table or similar for better perf with lots of logs

Expand All @@ -13,9 +14,13 @@ export function ComponentData() {
const component = world.components.find((component) => component.id === idParam);

// TODO: error message or redirect?
if (!component || !isStoreComponent(component)) return null;
if (!component) return null;

// key here is useful to force a re-render on component changes,
// otherwise state hangs around from previous render during navigation (entities)
return <ComponentDataTable key={component.id} component={component} />;
return isStoreComponent(component) ? (
<StoreComponentDataTable key={component.id} component={component} />
) : (
<ComponentDataTable key={component.id} component={component} />
);
}
30 changes: 13 additions & 17 deletions packages/dev-tools/src/recs/ComponentDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEntityQuery } from "@latticexyz/react";
import { Component, Has, Schema, getComponentValueStrict } from "@latticexyz/recs";
import { StoreComponentMetadata, decodeEntity } from "@latticexyz/store-sync/recs";
import { Component, Has, getComponentValueStrict, Type } from "@latticexyz/recs";
import { decodeEntity } from "@latticexyz/store-sync/recs";
import { serialize } from "../serialize";

// TODO: use react-table or similar for better perf with lots of logs

type Props = {
component: Component<Schema, StoreComponentMetadata>;
component: Component;
};

export function ComponentDataTable({ component }: Props) {
Expand All @@ -16,12 +17,8 @@ export function ComponentDataTable({ component }: Props) {
<table className="w-full -mx-1">
<thead className="sticky top-0 z-10 bg-slate-800 text-left">
<tr className="text-amber-200/80 font-mono">
{Object.keys(component.metadata.keySchema).map((name) => (
<th key={name} className="px-1 pt-1.5 font-normal">
{name}
</th>
))}
{Object.keys(component.metadata.valueSchema).map((name) => (
<th className="px-1 pt-1.5 font-normal">entity</th>
{Object.keys(component.schema).map((name) => (
<th key={name} className="px-1 pt-1.5 font-normal">
{name}
</th>
Expand All @@ -30,20 +27,19 @@ export function ComponentDataTable({ component }: Props) {
</thead>
<tbody className="font-mono text-xs">
{entities.map((entity) => {
const key = decodeEntity(component.metadata.keySchema, entity);
const value = getComponentValueStrict(component, entity);
return (
<tr key={entity}>
{Object.keys(component.metadata.keySchema).map((name) => (
<td key={name} className="px-1 whitespace-nowrap overflow-hidden text-ellipsis">
{String(key[name])}
</td>
))}
{Object.keys(component.metadata.valueSchema).map((name) => {
<td className="px-1 whitespace-nowrap overflow-hidden text-ellipsis">{entity}</td>
{Object.keys(component.schema).map((name) => {
const fieldValue = value[name];
return (
<td key={name} className="px-1 whitespace-nowrap overflow-hidden text-ellipsis">
{Array.isArray(fieldValue) ? fieldValue.map(String).join(", ") : String(fieldValue)}
{component.schema[name] === Type.T
? serialize(fieldValue)
: Array.isArray(fieldValue)
? fieldValue.map(String).join(", ")
: String(fieldValue)}
</td>
);
})}
Expand Down
17 changes: 9 additions & 8 deletions packages/dev-tools/src/recs/ComponentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import { NavButton } from "../NavButton";
import { useEffect, useRef } from "react";
import { twMerge } from "tailwind-merge";
import { useDevToolsContext } from "../DevToolsContext";
import { isStoreComponent } from "@latticexyz/store-sync/recs";
import { getComponentName } from "./getComponentName";

export function ComponentsPage() {
const { recsWorld: world } = useDevToolsContext();
if (!world) throw new Error("Missing recsWorld");

const components = world.components.filter(isStoreComponent);
const components = [...world.components].sort((a, b) => getComponentName(a).localeCompare(getComponentName(b)));

// TODO: lift up selected component so we can remember previous selection between tab nav
const { id: idParam } = useParams();
const selectedComponent = components.find((component) => component.id === idParam);
const selectedComponent = components.find((component) => component.id === idParam) ?? components[0];

const detailsRef = useRef<HTMLDetailsElement>(null);
const navigate = useNavigate();

useEffect(() => {
if (components.length && !selectedComponent) {
navigate(components[0].id);
if (idParam !== selectedComponent.id) {
navigate(selectedComponent.id);
}
}, [components, selectedComponent]);
}, [idParam, selectedComponent.id]);

useEffect(() => {
const listener = (event: MouseEvent) => {
Expand All @@ -45,7 +46,7 @@ export function ComponentsPage() {
<summary className="group pointer-events-auto cursor-pointer inline-flex">
<span className="inline-flex gap-2 px-3 py-2 items-center border-2 border-white/10 rounded group-hover:border-blue-700 group-hover:bg-blue-700 group-hover:text-white">
{selectedComponent ? (
<span className="font-mono">{selectedComponent.metadata.componentName}</span>
<span className="font-mono">{getComponentName(selectedComponent)}</span>
) : (
<span>Pick a component…</span>
)}
Expand All @@ -68,7 +69,7 @@ export function ComponentsPage() {
}
}}
>
{component.metadata.componentName}
{getComponentName(component)}
</NavButton>
))}
</div>
Expand Down
56 changes: 56 additions & 0 deletions packages/dev-tools/src/recs/StoreComponentDataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEntityQuery } from "@latticexyz/react";
import { Component, Has, Schema, getComponentValueStrict } from "@latticexyz/recs";
import { StoreComponentMetadata, decodeEntity } from "@latticexyz/store-sync/recs";

// TODO: use react-table or similar for better perf with lots of logs

type Props = {
component: Component<Schema, StoreComponentMetadata>;
};

export function StoreComponentDataTable({ component }: Props) {
// TODO: this breaks when navigating because its state still has entity IDs from prev page
const entities = useEntityQuery([Has(component)]);

return (
<table className="w-full -mx-1">
<thead className="sticky top-0 z-10 bg-slate-800 text-left">
<tr className="text-amber-200/80 font-mono">
{Object.keys(component.metadata.keySchema).map((name) => (
<th key={name} className="px-1 pt-1.5 font-normal">
{name}
</th>
))}
{Object.keys(component.metadata.valueSchema).map((name) => (
<th key={name} className="px-1 pt-1.5 font-normal">
{name}
</th>
))}
</tr>
</thead>
<tbody className="font-mono text-xs">
{entities.map((entity) => {
const key = decodeEntity(component.metadata.keySchema, entity);
const value = getComponentValueStrict(component, entity);
return (
<tr key={entity}>
{Object.keys(component.metadata.keySchema).map((name) => (
<td key={name} className="px-1 whitespace-nowrap overflow-hidden text-ellipsis">
{String(key[name])}
</td>
))}
{Object.keys(component.metadata.valueSchema).map((name) => {
const fieldValue = value[name];
return (
<td key={name} className="px-1 whitespace-nowrap overflow-hidden text-ellipsis">
{Array.isArray(fieldValue) ? fieldValue.map(String).join(", ") : String(fieldValue)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
}
5 changes: 5 additions & 0 deletions packages/dev-tools/src/recs/getComponentName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Component } from "@latticexyz/recs";

export function getComponentName(component: Component): string {
return String(component.metadata?.componentName ?? component.id);
}
10 changes: 5 additions & 5 deletions packages/dev-tools/src/summary/ComponentsSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { World } from "@latticexyz/recs";
import { NavButton } from "../NavButton";
import { isStoreComponent } from "@latticexyz/store-sync/recs";
import { getComponentName } from "../recs/getComponentName";

type Props = {
world: World;
};

export function ComponentsSummary({ world }: Props) {
const componentsWithName = world.components.filter(isStoreComponent);
const components = [...world.components].sort((a, b) => getComponentName(a).localeCompare(getComponentName(b)));
return (
<>
{componentsWithName.length ? (
{components.length ? (
<>
<div className="flex flex-col gap-1 items-start">
{componentsWithName.map((component) => (
{components.map((component) => (
<NavButton
key={component.id}
to={`/components/${component.id}`}
className="font-mono text-xs hover:text-white"
>
{String(component.metadata.componentName)}
{getComponentName(component)}
</NavButton>
))}
</div>
Expand Down
25 changes: 5 additions & 20 deletions packages/store-sync/src/recs/defineInternalComponents.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { World, defineComponent, Type, Component, Schema } from "@latticexyz/recs";
import { World, defineComponent, Type, Component, Schema, Metadata } from "@latticexyz/recs";
import { Table } from "../common";
import { StoreComponentMetadata } from "./common";

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function defineInternalComponents(world: World) {
return {
TableMetadata: defineComponent<{ table: Type.T }, StoreComponentMetadata, Table>(
TableMetadata: defineComponent<{ table: Type.T }, Metadata, Table>(
world,
{ table: Type.T },
{
metadata: {
componentName: "TableMetadata",
tableName: "recs:TableMetadata",
keySchema: {},
valueSchema: {},
},
}
{ metadata: { componentName: "TableMetadata" } }
),
SyncProgress: defineComponent(
world,
Expand All @@ -26,14 +18,7 @@ export function defineInternalComponents(world: World) {
latestBlockNumber: Type.BigInt,
lastBlockNumberProcessed: Type.BigInt,
},
{
metadata: {
componentName: "SyncProgress",
tableName: "recs:SyncProgress",
keySchema: {},
valueSchema: {},
},
}
{ metadata: { componentName: "SyncProgress" } }
),
} as const satisfies Record<string, Component<Schema, StoreComponentMetadata>>;
} as const satisfies Record<string, Component<Schema, Metadata>>;
}

0 comments on commit 5294a7d

Please sign in to comment.