diff --git a/desktop/src/components/db-viewer/HypertableDetail.test.tsx b/desktop/src/components/db-viewer/HypertableDetail.test.tsx new file mode 100644 index 0000000..c49576e --- /dev/null +++ b/desktop/src/components/db-viewer/HypertableDetail.test.tsx @@ -0,0 +1,43 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { HypertableDetail } from "./HypertableDetail"; +import type { HypertableInfo } from "../../lib/types"; + +const item: HypertableInfo = { + name: "metrics", + schema: "public", + num_dimensions: 2, + compression_enabled: true, + num_chunks: 42, + total_size_bytes: 1048576, +}; + +describe("HypertableDetail", () => { + it("renders dimensions, compression, chunk count and human size", () => { + render(); + expect(screen.getByText("metrics")).toBeTruthy(); + expect(screen.getByText(/2 dimensions/)).toBeTruthy(); + expect(screen.getByText(/compression enabled/)).toBeTruthy(); + expect(screen.getByText(/42 chunks/)).toBeTruthy(); + expect(screen.getByText(/1\.0 MB/)).toBeTruthy(); + }); + + it("shows the DDL/restore caveat", () => { + render(); + expect(screen.getByText(/create_hypertable/)).toBeTruthy(); + }); + + it("opens the underlying table on click", () => { + const onOpenTable = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: /view table/i })); + expect(onOpenTable).toHaveBeenCalledWith("public", "metrics"); + }); + + it("marks compression as disabled when off", () => { + render( + , + ); + expect(screen.getByText(/compression disabled/)).toBeTruthy(); + }); +}); diff --git a/desktop/src/components/db-viewer/HypertableDetail.tsx b/desktop/src/components/db-viewer/HypertableDetail.tsx new file mode 100644 index 0000000..b7d5e54 --- /dev/null +++ b/desktop/src/components/db-viewer/HypertableDetail.tsx @@ -0,0 +1,48 @@ +import { Table2 } from "lucide-react"; +import type { HypertableInfo } from "../../lib/types"; + +function humanBytes(bytes: number | null): string { + if (bytes === null) return "unknown size"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let v = bytes; + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i += 1; + } + return `${v >= 10 || i === 0 ? v.toFixed(0) : v.toFixed(1)} ${units[i]}`; +} + +export function HypertableDetail({ + item, + onOpenTable, +}: { + item: HypertableInfo; + onOpenTable: (schema: string, table: string) => void; +}) { + return ( +
+
{item.name}
+
+ {item.num_dimensions} dimensions + + {item.compression_enabled ? "compression enabled" : "compression disabled"} + + {item.num_chunks} chunks + {humanBytes(item.total_size_bytes)} +
+ +

+ Note: Copy DDL exports the table as plain SQL plus its create_hypertable call. A + full-fidelity restore needs TimescaleDB pre/post-restore hooks, which Gridline does + not run. +

+
+ ); +} diff --git a/desktop/src/components/db-viewer/ObjectTree.tsx b/desktop/src/components/db-viewer/ObjectTree.tsx index a70a168..37ebc42 100644 --- a/desktop/src/components/db-viewer/ObjectTree.tsx +++ b/desktop/src/components/db-viewer/ObjectTree.tsx @@ -3,6 +3,7 @@ import { ChevronRight, ChevronDown, FunctionSquare, GitBranch, ListOrdered, Tag, import { useDbViewerStore } from "../../stores/dbViewerStore"; import * as cmd from "../../lib/commands"; import type { FunctionInfo, TriggerInfo, SequenceInfo, EnumInfo, ExtensionInfo, HypertableInfo } from "../../lib/types"; +import { HypertableDetail } from "./HypertableDetail"; type ObjectType = "functions" | "triggers" | "sequences" | "enums" | "extensions" | "hypertables"; @@ -336,6 +337,16 @@ export function ObjectTree({ type, connectionId }: ObjectTreeProps) { ); })()} + + {type === "hypertables" && (() => { + const h = item as HypertableInfo; + return ( + useDbViewerStore.getState().openTab(schema, table)} + /> + ); + })()} )}