Skip to content

Commit

Permalink
feat: added database schema drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
vwh committed Nov 8, 2024
1 parent 1391371 commit 108ace8
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 70 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 55 additions & 5 deletions src/components/database/table-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,75 @@ const TableHeadCell: React.FC<{
</TableHead>
));

const hexToDataUrl = (hex: string): string => {
const bytes = new Uint8Array(
hex.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16))
);
const blob = new Blob([bytes], { type: "image/jpeg" });
return URL.createObjectURL(blob);
};

const TableBodyCell: React.FC<{ value: any; dataType?: string }> = memo(
({ value, dataType }) => {
const { dateFormatValue } = useSQLiteStore();

const CellContent = () => {
const isBlob = dataType?.toUpperCase() === "BLOB";

const content = useMemo(() => {
if (!value) {
return <span className="italic text-gray-400">NULL</span>;
}

if (dataType && isDate(dataType)) {
if (dateFormats[dateFormatValue]) {
return dateFormats[dateFormatValue].func(value);
}
return value;
}
return value;
};

const stringValue = typeof value === "string" ? value : String(value);
return stringValue.length > 40
? `${stringValue.slice(0, 40)}...`
: stringValue;
}, [value, dataType, dateFormatValue]);

return (
<TableCell dataType={dataType} className="px-5 py-[11px] text-sm">
{CellContent()}
<TableCell className="px-5 py-[11px] text-sm">
<HoverCard>
<HoverCardTrigger asChild>
<span className="cursor-pointer hover:underline">
{isBlob ? (
<span className="italic opacity-40">BLOB</span>
) : (
content
)}
</span>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start">
<div className="flex flex-col justify-center gap-1">
{isBlob && typeof value === "string" ? (
<>
<img
src={hexToDataUrl(value)}
alt="BLOB content"
className="flex max-h-40 flex-col items-center justify-center gap-2 rounded object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
<span className="text-sm text-muted-foreground">
Blob length: {value.length}
</span>
</>
) : (
<span className="max-w-full break-words">{content}</span>
)}
<Badge className="w-full self-start text-center text-xs font-semibold">
{dataType || "Unknown"}
</Badge>
</div>
</HoverCardContent>
</HoverCard>
</TableCell>
);
}
Expand Down
21 changes: 16 additions & 5 deletions src/components/database/upper-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button";
import StatusMessage from "@/components/stats-message";
import ExportButtons from "@/components/settings/export-buttons";
import Settings from "@/components/settings/settings-drawer";
import DatabaseSchema from "@/components/settings/database-schema-drawer";
import ThemeModeToggle from "@/components/settings/theme-mode-toggle";
import PageSelect from "./page-select";
import TableSelect from "./table-select";
Expand Down Expand Up @@ -205,9 +206,20 @@ export function DBTable() {
return (
<>
<div className="flex flex-col gap-3 pb-8">
<p className="text-center text-sm text-muted-foreground">
{databaseData.name}, ({databaseData.sizeAsString})
</p>
<section className="flex w-full items-center justify-between gap-4 rounded-lg bg-gray-100 p-4 text-sm shadow-sm dark:bg-gray-700">
<div className="flex items-center gap-1">
<div className="max-w-[160px] overflow-hidden text-ellipsis whitespace-nowrap font-medium">
{databaseData.name}
</div>
<div className="hidden text-gray-500 dark:text-gray-400 sm:block">
({databaseData.sizeAsString})
</div>
</div>
<div className="flex items-center gap-1">
<ThemeModeToggle />
<Settings />
</div>
</section>
<section className="rounded-lg bg-gray-100 p-4 shadow-sm dark:bg-gray-700">
<div className="mb-[5px] flex items-center justify-between gap-[6px] pb-[3px]">
<MemoizedTableSelect />
Expand All @@ -220,8 +232,7 @@ export function DBTable() {
>
{MemoizedExpandIcon}
</Button>
<ThemeModeToggle />
<Settings />
<DatabaseSchema />
</div>
</div>
{MemoizedQueryInput}
Expand Down
6 changes: 5 additions & 1 deletion src/components/landing/features.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { memo } from "react";

import {
ShieldIcon,
ZapIcon,
Expand All @@ -7,7 +9,7 @@ import {
AppWindowIcon
} from "lucide-react";

export default function Features() {
export function Features() {
return (
<>
<section className="rounded bg-gradient-to-r py-6 shadow-md dark:from-gray-800 dark:to-indigo-900 md:py-10">
Expand Down Expand Up @@ -72,3 +74,5 @@ function Feature({ icon: Icon, title, description }: FeatureProps) {
</div>
);
}

export default memo(Features);
95 changes: 95 additions & 0 deletions src/components/settings/database-schema-drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import useSQLiteStore from "@/store/useSQLiteStore";
import { useMemo, memo } from "react";

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger
} from "@/components/ui/drawer";
import { ColumnIcon } from "@/components/database/table-decorations";
import { Button } from "../ui/button";
import { TableIcon } from "lucide-react";

function DatabaseSchema() {
const { tableSchemas } = useSQLiteStore();

const renderedTables = useMemo(() => {
return Object.keys(tableSchemas).map((tableName) => (
<section key={tableName}>
<h2 className="rounded-t-lg bg-gray-100 p-2 dark:bg-gray-700">
{tableName}
</h2>
<div className="overflow-hidden rounded-b-lg border border-gray-200 dark:border dark:border-gray-700">
<Table>
<TableHeader>
<TableRow>
<TableHead>Column</TableHead>
<TableHead>Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Object.entries(tableSchemas[tableName]).map(
([columnName, columnData]) => (
<TableRow key={columnName}>
<TableCell>
<div className="flex cursor-pointer items-center space-x-1">
<span className="max-w-[200px] overflow-hidden truncate text-ellipsis whitespace-nowrap">
{columnName}
</span>
<ColumnIcon columnSchema={columnData} />
</div>
</TableCell>
<TableCell>{columnData.type || "Unknown"}</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
</section>
));
}, [tableSchemas]);

return (
<Drawer key="table-schema-drawer">
<DrawerTrigger asChild>
<Button className="grow" title="Open table schema drawer">
<TableIcon className="h-5 w-5" />
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="hidden">
<DrawerTitle>Settings</DrawerTitle>
<DrawerDescription>Change settings.</DrawerDescription>
</DrawerHeader>
<section className="flex grow flex-col gap-2 px-4 md:px-0">
<div className="mx-auto flex max-h-[500px] w-full max-w-xl flex-col gap-3 overflow-y-auto p-2">
{renderedTables}
</div>
<div className="mx-auto w-full max-w-sm">
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</div>
</section>
</DrawerContent>
</Drawer>
);
}

export default memo(DatabaseSchema);
2 changes: 1 addition & 1 deletion src/components/settings/settings-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function Settings() {
<DrawerTitle>Settings</DrawerTitle>
<DrawerDescription>Change settings.</DrawerDescription>
</DrawerHeader>
<div className="mx-auto flex w-full max-w-sm flex-col gap-3">
<div className="mx-auto flex w-full max-w-sm flex-col gap-3 px-4 md:px-0">
<RowsPerPageSection
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleRowsPerPageChange}
Expand Down
68 changes: 10 additions & 58 deletions src/components/ui/table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as React from "react";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
import { Badge } from "./badge";
import { cn } from "@/lib/utils";

const Table = React.forwardRef<
Expand Down Expand Up @@ -83,29 +81,19 @@ const TableHead = React.forwardRef<
TableHead.displayName = "TableHead";

// Function to convert hex blob string to data URL
const hexToDataUrl = (hex: string): string => {
const bytes = new Uint8Array(
hex.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16))
);
const blob = new Blob([bytes], { type: "image/jpeg" });
return URL.createObjectURL(blob);
};
// const hexToDataUrl = (hex: string): string => {
// const bytes = new Uint8Array(
// hex.match(/.{1,2}/g)!.map((byte) => Number.parseInt(byte, 16))
// );
// const blob = new Blob([bytes], { type: "image/jpeg" });
// return URL.createObjectURL(blob);
// };

interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
dataType?: string;
}
type TableCellProps = React.TdHTMLAttributes<HTMLTableCellElement>;

const TableCell = React.memo(
React.forwardRef<HTMLTableCellElement, TableCellProps>(
({ className, children, dataType, ...props }, ref) => {
const isBlob = dataType?.toUpperCase() === "BLOB";
const content = React.useMemo(() => {
if (typeof children === "string" && children.length > 40) {
return `${children.slice(0, 40)}...`;
}
return children;
}, [children]);

({ className, children, ...props }, ref) => {
return (
<td
ref={ref}
Expand All @@ -115,43 +103,7 @@ const TableCell = React.memo(
)}
{...props}
>
<HoverCard>
<HoverCardTrigger asChild>
<span className="cursor-pointer hover:underline">
{isBlob ? (
<span className="italic opacity-40">BLOB</span>
) : (
content
)}
</span>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start">
<div className="flex flex-col justify-center gap-1">
{isBlob && typeof children === "string" ? (
<>
<img
src={hexToDataUrl(children)}
alt="BLOB content"
className="flex max-h-40 flex-col items-center justify-center gap-2 rounded object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
<span className="text-sm text-muted-foreground">
Blob length: {children.length}
</span>
</>
) : (
<span className="max-w-full break-words">{children}</span>
)}
{
<Badge className="w-full self-start text-center text-xs font-semibold">
{dataType || "Unknown"}
</Badge>
}
</div>
</HoverCardContent>
</HoverCard>
{children}
</td>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface TableInfo {
type: string;
isPrimaryKey: boolean;
isForeignKey: boolean;
nullable: boolean;
};
};
}
Expand Down

0 comments on commit 108ace8

Please sign in to comment.