Skip to content

Commit 5e700f9

Browse files
herzadinataBOCOVO
authored andcommitted
feat(dblm-ext): search table & column feature added
1 parent e087d3b commit 5e700f9

File tree

4 files changed

+243
-8
lines changed

4 files changed

+243
-8
lines changed

packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramViewer.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
} from "shared/types/tableSchema";
66

77
import EmptyTableMessage from "../Messages/EmptyTableMessage";
8+
import Search from "../Search/Search";
89

9-
import Tables from "./Tables";
10-
import RelationsConnections from "./Connections";
1110
import DiagramWrapper from "./DiagramWrapper";
11+
import RelationsConnections from "./Connections";
12+
import Tables from "./Tables";
1213

1314
import TablesPositionsProvider from "@/providers/TablesPositionsProvider";
1415
import MainProviders from "@/providers/MainProviders";
@@ -29,11 +30,13 @@ const DiagramViewer = ({ refs, tables, enums }: DiagramViewerProps) => {
2930
<TableLevelDetailProvider>
3031
<TablesPositionsProvider tables={tables} refs={refs}>
3132
<MainProviders tables={tables} enums={enums}>
32-
<DiagramWrapper>
33-
<RelationsConnections refs={refs} />
34-
35-
<Tables tables={tables} />
36-
</DiagramWrapper>
33+
<>
34+
<Search tables={tables} />
35+
<DiagramWrapper>
36+
<RelationsConnections refs={refs} />
37+
<Tables tables={tables} />
38+
</DiagramWrapper>
39+
</>
3740
</MainProviders>
3841
</TablesPositionsProvider>
3942
</TableLevelDetailProvider>

packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramWrapper.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { useStageStartingState } from "@/hooks/stage";
1515
import { stageStateStore } from "@/stores/stagesState";
1616
import { useScrollDirectionContext } from "@/hooks/scrollDirection";
1717
import { ScrollDirection } from "@/types/scrollDirection";
18+
import eventEmitter from "@/events-emitter";
19+
import { tableCoordsStore } from "@/stores/tableCoords";
1820

1921
interface DiagramWrapperProps {
2022
children: ReactNode;
@@ -111,6 +113,66 @@ const DiagramWrapper = ({ children }: DiagramWrapperProps) => {
111113
}
112114
};
113115

116+
/**
117+
* Center handler: listen for requests to center the stage on a given table
118+
* when the search option is clicked.
119+
*/
120+
useEffect(() => {
121+
const handler = ({ tableName }: { tableName: string }) => {
122+
if (stageRef.current == null) return;
123+
124+
const stage = stageRef.current;
125+
const container = stage.container();
126+
const containerWidth = container.offsetWidth;
127+
const containerHeight = container.offsetHeight;
128+
129+
// Try to find the node by name first
130+
const nodeName = `table-${tableName.replace(/\s+/g, "_")}`;
131+
// Konva's findOne accepts a selector like `.name`
132+
const node = stage.findOne(`.${nodeName}`);
133+
134+
// Get bounding rect relative to stage
135+
let rect: { x: number; y: number; width: number; height: number };
136+
if (node != null && typeof (node as any).getClientRect === "function") {
137+
rect = (node as any).getClientRect({ relativeTo: stage });
138+
} else {
139+
// Fallback to stored coords (top-left) and assume a small box
140+
const coords = tableCoordsStore.getCoords(tableName);
141+
rect = { x: coords.x, y: coords.y, width: 200, height: 100 };
142+
}
143+
144+
const scale = stage.scaleX();
145+
146+
const newPos = {
147+
x: containerWidth / 2 - (rect.x + rect.width / 2) * scale,
148+
y: containerHeight / 2 - (rect.y + rect.height / 2) * scale,
149+
};
150+
151+
// animate stage position for a smooth pan
152+
try {
153+
(stage as any).to({
154+
x: newPos.x,
155+
y: newPos.y,
156+
duration: 0.45,
157+
onFinish: () => {
158+
stage.batchDraw();
159+
stageStateStore.set({ scale: stage.scaleX(), position: newPos });
160+
},
161+
});
162+
} catch (e) {
163+
// fallback to immediate set
164+
stage.position(newPos);
165+
stage.batchDraw();
166+
stageStateStore.set({ scale: stage.scaleX(), position: newPos });
167+
}
168+
};
169+
170+
eventEmitter.on("table:center", handler);
171+
return () => {
172+
eventEmitter.off("table:center", handler);
173+
};
174+
}, []);
175+
114176
return (
115177
<main
116178
className={`relative flex flex-col items-center ${theme === Theme.dark ? "dark" : ""}`}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { useState, useMemo, useRef, useEffect } from "react";
2+
import { type JSONTableTable } from "shared/types/tableSchema";
3+
4+
import eventEmitter from "@/events-emitter";
5+
import { useTablesInfo } from "@/hooks/table";
6+
7+
interface SearchResult {
8+
type: "table" | "column";
9+
tableName: string;
10+
name: string;
11+
}
12+
13+
interface SearchProps {
14+
tables: JSONTableTable[];
15+
}
16+
17+
/**
18+
* This is the search bar component placed on the top-right corner
19+
* of the stage, where you can search table or column.
20+
*/
21+
const Search = ({ tables }: SearchProps) => {
22+
const [search, setSearch] = useState("");
23+
const [isOpen, setIsOpen] = useState(false);
24+
const { setHoveredTableName } = useTablesInfo();
25+
const dropdownRef = useRef<HTMLDivElement>(null);
26+
27+
const searchResults = useMemo(() => {
28+
if (search === "") return [];
29+
30+
const results: SearchResult[] = [];
31+
const searchLower = search.toLowerCase();
32+
33+
tables.forEach((table) => {
34+
// Search in table names
35+
if (table.name.toLowerCase().includes(searchLower)) {
36+
results.push({
37+
type: "table",
38+
tableName: table.name,
39+
name: table.name,
40+
});
41+
}
42+
43+
// Search in column names
44+
table.fields.forEach((field) => {
45+
if (field.name.toLowerCase().includes(searchLower)) {
46+
results.push({
47+
type: "column",
48+
tableName: table.name,
49+
name: field.name,
50+
});
51+
}
52+
});
53+
});
54+
55+
return results;
56+
}, [tables, search]);
57+
58+
const handleSelect = (result: SearchResult) => {
59+
setHoveredTableName(result.tableName);
60+
// Center the diagram on the selected table and ask the table to highlight itself
61+
eventEmitter.emit("table:center", { tableName: result.tableName });
62+
eventEmitter.emit(`highlight:table:${result.tableName}`);
63+
64+
setIsOpen(false);
65+
setSearch("");
66+
};
67+
68+
// Close dropdown when clicking outside
69+
useEffect(() => {
70+
const handleClickOutside = (event: MouseEvent) => {
71+
if (
72+
dropdownRef.current != null &&
73+
!dropdownRef.current.contains(event.target as Node)
74+
) {
75+
setIsOpen(false);
76+
}
77+
};
78+
79+
document.addEventListener("mousedown", handleClickOutside);
80+
return () => {
81+
document.removeEventListener("mousedown", handleClickOutside);
82+
};
83+
}, []);
84+
85+
return (
86+
<div className="fixed top-4 right-4 z-50" ref={dropdownRef}>
87+
<div className="relative">
88+
<input
89+
type="text"
90+
value={search}
91+
onChange={(e) => {
92+
setSearch(e.target.value);
93+
setIsOpen(true);
94+
}}
95+
placeholder="Search tables and columns..."
96+
className="w-64 px-4 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
97+
/>
98+
99+
{isOpen && searchResults.length > 0 && (
100+
<div className="absolute top-full mt-1 w-full bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 max-h-60 overflow-y-auto">
101+
{searchResults.map((result, index) => (
102+
<button
103+
key={`${result.type}-${result.tableName}-${result.name}-${index}`}
104+
onClick={() => {
105+
handleSelect(result);
106+
}}
107+
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-600 flex flex-col items-start"
108+
>
109+
<div className="flex space-x-2 w-full items-start">
110+
<span className="text-xs text-gray-500 dark:text-gray-400 mt-[2px]">
111+
{result.type === "table" ? "📋" : "🔤"}
112+
</span>
113+
<span className="font-medium break-all">{result.name}</span>
114+
</div>
115+
{result.type === "column" && (
116+
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400 break-all">
117+
in {result.tableName}
118+
</div>
119+
)}
120+
</button>
121+
))}
122+
</div>
123+
)}
124+
</div>
125+
</div>
126+
);
127+
};
128+
129+
export default Search;

packages/json-table-schema-visualizer/src/components/Table.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
TABLE_COLOR_HEIGHT,
1515
TABLE_HEADER_HEIGHT,
1616
} from "@/constants/sizing";
17-
import { useThemeColors } from "@/hooks/theme";
17+
import { useThemeColors, useThemeContext } from "@/hooks/theme";
18+
import { Theme } from "@/types/theme";
1819
import eventEmitter from "@/events-emitter";
1920
import { computeTableDragEventName } from "@/utils/eventName";
2021
import {
@@ -34,6 +35,8 @@ const Table = ({ fields, name }: TableProps) => {
3435
const themeColors = useThemeColors();
3536
const { detailLevel } = useTableDetailLevel();
3637
const tableRef = useRef<null | Konva.Group>(null);
38+
const highlightRef = useRef<null | Konva.Rect>(null);
39+
const { theme } = useThemeContext();
3740
const { setHoveredTableName } = useTablesInfo();
3841
const { x: tableX, y: tableY } = useTableDefaultPosition(name);
3942
const tablePreferredWidth = useTableWidth();
@@ -56,6 +59,31 @@ const Table = ({ fields, name }: TableProps) => {
5659

5760
const tableDragEventName = computeTableDragEventName(name);
5861

62+
// Subscribe to highlight events for this table and animate the border
63+
useEffect(() => {
64+
const eventName = `highlight:table:${name}`;
65+
const handler = () => {
66+
// Animate using Konva to make transition smooth
67+
const rect = highlightRef.current;
68+
if (rect != null) {
69+
// Set stroke color according to theme
70+
const color = theme === Theme.dark ? "#FBBF24" : "#3B82F6";
71+
rect.stroke(color);
72+
rect.to({ strokeWidth: 3, opacity: 1, duration: 0.18 });
73+
74+
// After 2s, animate out
75+
setTimeout(() => {
76+
rect.to({ strokeWidth: 0, opacity: 0, duration: 0.28 });
77+
}, 2000);
78+
}
79+
};
80+
81+
eventEmitter.on(eventName, handler);
82+
return () => {
83+
eventEmitter.off(eventName, handler);
84+
};
85+
}, [name, theme]);
86+
5987
const propagateCoordinates = (node: Konva.Group) => {
6088
const tableCoords = { x: node.x(), y: node.y() };
6189
eventEmitter.emit(tableDragEventName, tableCoords);
@@ -83,6 +111,7 @@ const Table = ({ fields, name }: TableProps) => {
83111

84112
return (
85113
<Group
114+
name={`table-${name.replace(/\s+/g, "_")}`}
86115
ref={tableRef}
87116
draggable
88117
onDragMove={handleOnDrag}
@@ -122,6 +151,18 @@ const Table = ({ fields, name }: TableProps) => {
122151
) : (
123152
<></>
124153
)}
154+
155+
{/* Highlight border temporarily when the a search option is clicked */}
156+
<Rect
157+
ref={highlightRef}
158+
height={tableHeight}
159+
width={tablePreferredWidth}
160+
cornerRadius={PADDINGS.sm}
161+
stroke={theme === Theme.dark ? "#FBBF24" : "#3B82F6"}
162+
strokeWidth={0}
163+
opacity={0}
164+
listening={false}
165+
/>
125166
</Group>
126167
);
127168
};

0 commit comments

Comments
 (0)