Add network and syncing status and made the database controller resizebale

This commit is contained in:
KarimTamani
2025-05-26 15:04:05 +01:00
parent 4a3f266ad1
commit 6d41e73af8
10 changed files with 224 additions and 33 deletions
+1
View File
@@ -21,6 +21,7 @@
"@react-aria/visually-hidden": "3.8.21",
"@react-types/shared": "3.28.0",
"@xyflow/react": "^12.6.3",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.43.1",
"elkjs": "^0.10.0",
"fast-json-patch": "^3.1.1",
+142
View File
@@ -0,0 +1,142 @@
import { usePowerSync } from "@powersync/react";
import { Wifi, WifiOff } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { cn, Divider, Progress } from "@heroui/react";
import { differenceInHours, differenceInMinutes, lastDayOfYear } from "date-fns";
const ConnectionStatus: React.FC = () => {
const powerSync = usePowerSync();
const [syncStatus, setSyncStatus] = useState(powerSync.currentStatus);
const [downloadProgress, setDownloadProgress] = useState<number | undefined>(undefined);
const clearProgressHandler: any = useRef(undefined);
const [lastUploadAt, setLastUploadAt] = useState<Date | undefined>(() => {
const last_upload_at = localStorage.getItem("last_upload_at");
if (last_upload_at)
return new Date(last_upload_at);
return undefined;
})
useEffect(() => {
const getLastUploadAtValue = (event: any) => {
const last_upload_at = localStorage.getItem("last_upload_at");
if (last_upload_at)
setLastUploadAt(new Date(last_upload_at));
}
window.addEventListener("storage", getLastUploadAtValue);
return () => {
window.removeEventListener('storage', getLastUploadAtValue);
}
}, [])
useEffect(() => {
const l = powerSync.registerListener({
statusChanged: (status) => {
setSyncStatus(status);
},
});
return () => l?.();
}, [powerSync]);
useEffect(() => {
if (syncStatus.downloadProgress?.downloadedOperations && syncStatus.downloadProgress?.totalOperations) {
const progress: number = Math.round(syncStatus.downloadProgress.downloadedOperations / syncStatus.downloadProgress.totalOperations * 100);
if (progress <= 100) {
setDownloadProgress(progress);
}
if (downloadProgress == 100 && !clearProgressHandler.current) {
clearProgressHandler.current = setTimeout(() => {
setDownloadProgress(undefined);
clearProgressHandler.current = undefined;
}, 700)
}
}
}, [syncStatus.downloadProgress])
const deltaUploadTime = useMemo(() => {
if (lastUploadAt) {
const currentDate = new Date();
const minutes = differenceInMinutes(currentDate, lastUploadAt)
const hours = differenceInHours(currentDate, lastUploadAt);
if (hours > 0 && hours < 24) {
return `${hours} hour ago`
}
if (minutes > 0 && minutes < 60)
return `${minutes} min ago`
if (hours > 24) {
return `${lastUploadAt.toLocaleDateString()}`
}
}
return "Just now";
}, [lastUploadAt]);
return (
<div className="flex items-center ">
<Tooltip>
<TooltipTrigger asChild>
<div className="border-1 flex gap-2 p-2 px-3 rounded-lg text-xs relative overflow-hidden dark:border-default ">
<span className={cn("flex gap-2 font-semibold", syncStatus.connected ? "text-success-400" : "text-danger-400")}>
{
syncStatus.connected ?
<Wifi className="size-4 " /> :
<WifiOff className="size-4 " />
}
{
syncStatus.connected ? "Online" : "Offline"
}
</span>
<Divider orientation="vertical" className="h-4" />
{
downloadProgress !== undefined ?
<>
<span className="text-slate-500 dark:text-white w-[96px] truncate">
Saving ...
</span>
<Progress
classNames={{
base: "max-w-md absolute bottom-[-2px] left-0 w-full ",
track: " bg-transparent",
indicator: "bg-gradient-to-r from-secondary-200 to-primary ",
}}
radius="sm"
aria-label="upload progress"
showValueLabel={false}
size="sm"
value={downloadProgress}
/>
</>
:
<span className="text-slate-500 dark:text-white w-[96px] truncate font-semibold" >
Saved {deltaUploadTime}
</span>
}
</div>
</TooltipTrigger>
<TooltipContent className="dark:bg-default-900">
Last synced {syncStatus.lastSyncedAt?.toISOString()}
</TooltipContent>
</Tooltip>
</div>
)
}
export default ConnectionStatus;
+9 -4
View File
@@ -1,6 +1,7 @@
import { NavbarBrand, NavbarContent, Navbar as NavbarContainer, Image, Dropdown, DropdownTrigger, Button, DropdownMenu, DropdownItem } from "@heroui/react";
import Menu from "../menu/menu";
import { useTheme } from "next-themes";
import ConnectionStatus from "./connection-status";
interface Props {
@@ -8,7 +9,7 @@ interface Props {
const Navbar: React.FC<Props> = ({ }) => {
const {resolvedTheme} = useTheme() ;
const { resolvedTheme } = useTheme();
return (
@@ -21,9 +22,13 @@ const Navbar: React.FC<Props> = ({ }) => {
/>
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
<div className="ml-4">
<Menu/>
<div className="ml-4 w-full">
<div className="flex w-full justify-between">
<Menu />
<ConnectionStatus />
</div>
</div>
</nav>
+8 -7
View File
@@ -33,13 +33,13 @@ import { AlertTriangle } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
const DatabasePage: React.FC<never> = () => {
const { t } = useTranslation() ;
const { t } = useTranslation();
// Extract database state and operations
const { database, getField } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
@@ -91,7 +91,7 @@ const DatabasePage: React.FC<never> = () => {
}
setIsConnectionInProgress(false);
}, [database , t ]);
}, [database, t]);
// Called when nodes are updated (position changes or removed)
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
@@ -156,9 +156,10 @@ const DatabasePage: React.FC<never> = () => {
return (
<div className="w-full h-screen flex relative">
<DBController />
<div className="w-full h-screen flex relative overflow-hidden">
<div>
<DBController />
</div>
<div className="relative w-full h-full">
<ReactFlow
nodes={nodes}
@@ -194,7 +195,7 @@ const DatabasePage: React.FC<never> = () => {
/>
</Controls >
<Background className="bg-default/40 dark:bg-black" />
<Background className="bg-background dark:bg-black" />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
@@ -1,19 +1,39 @@
import React from "react"
import React, { useState } from "react"
import { Outlet } from "react-router-dom"
import { ResizableBox as ResizableBoxRaw } from 'react-resizable';
import 'react-resizable/css/styles.css';
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
interface Props {
interface Props {
}
const DBController : React.FC<Props> = ({ }) => {
console.log ("re-render db controller")
return(
<div className="w-[640px] h-full bg-background border-r p-2 pt-[52px] border-default-100">
<Outlet/>
</div>
const DBController: React.FC<Props> = ({ }) => {
const [width, setWidth] = useState<number>(512);
const onResize = (event: any, params: any) => {
setWidth(params.size.width);
}
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full"
minConstraints={[512]}
axis="x"
handle={
<div className="w-[6px] h-full absolute right-0 top-0 cursor-ew-resize">
</div>
}
>
<div className="min-w-[512px] w-full h-full bg-background border-r p-2 pt-[52px] border-default-100">
<Outlet />
</div>
</ResizableBox>
)
}
@@ -21,7 +21,7 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
const { t } = useTranslation();
const changeCardinality = (keys: SharedSelection) => {
@@ -32,18 +32,29 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
} as RelationshipInsertType);
setCardinality(keys as any);
}
}
const removeRelationship = () => {
deleteRelationship(relationship.id);
}
useEffect(() => {
setCardinality(new Set([relationship.cardinality])) ;
} , [relationship.cardinality])
setCardinality(new Set([relationship.cardinality]));
}, [relationship.cardinality])
if (!relationship.sourceTable || !relationship.targetTable)
useEffect(() => {
console.log("re-render relationship controlelr", name);
}, [relationship])
if (!relationship.sourceTable || !relationship.targetTable)
return;
return (
<div className="w-full p-2 space-y-4">
<div className="flex">
@@ -92,6 +103,7 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
className="w-full"
size="sm"
variant="bordered"
label="cardinality"
selectedKeys={cardinality}
onSelectionChange={changeCardinality}
@@ -4,9 +4,9 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import hash from "object-hash";
interface RelationshipAccordionHeaderProps {
@@ -36,15 +36,15 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
}
const onDeleteRelationship = () => {
deleteRelationship(relationship.id);
setPopOverOpen(false);
}
useEffect(() => {
setName(relationship.name ? relationship.name : defaultName);
}, [relationship.name])
}, [relationship.name]);
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
@@ -146,4 +146,6 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
}
export default RelationshipAccordionHeader;
export default React.memo(RelationshipAccordionHeader, (previousState, newState) => {
return hash(previousState) == hash(newState);
});
@@ -25,7 +25,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
const [isValid, setIsValid] = useState<boolean>(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { database } = useDatabase();
const { createRelationship, } = useDatabaseOperations();
const { createRelationship } = useDatabaseOperations();
const { relationships } = database;
const { t } = useTranslation();
@@ -50,7 +50,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
setSelectedRelationship(new Set([focusedRelationshipId]) as any);
}
}, [focusedRelationshipId])
}, [focusedRelationshipId]) ;
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -35,8 +35,12 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
useEffect(() => {
powerSync.init();
powerSync.connect(connector);
}, [powerSync, connector])
return (
<Suspense fallback={<CircularProgress />}>
<PowerSyncContext.Provider value={powerSync}>
+4
View File
@@ -98,6 +98,10 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
? await this.getCheckpoint(this._clientId)
: undefined
);
localStorage.setItem("last_upload_at", new Date().toISOString());
window.dispatchEvent(new StorageEvent("storage", { key: "lastUploadAt" }));
} catch (ex: any) {
console.debug(ex);
throw ex;