From 6d41e73af879975d8f767a850dbf80e7b598e440 Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Mon, 26 May 2025 15:04:05 +0100 Subject: [PATCH] Add network and syncing status and made the database controller resizebale --- package.json | 1 + src/components/navbar/connection-status.tsx | 142 ++++++++++++++++++ src/components/navbar/navbar.tsx | 13 +- src/pages/database/database-page.tsx | 15 +- .../database/db-controller/db-controller.tsx | 38 +++-- .../relationship-accordion-body.tsx | 22 ++- .../relationship-accordion-header.tsx | 14 +- .../relationship-controller.tsx | 4 +- src/providers/sync-provider/sync-provider.tsx | 4 + src/utils/stackrender-connector.ts | 4 + 10 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 src/components/navbar/connection-status.tsx diff --git a/package.json b/package.json index 5981a1f..55fdc1f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/navbar/connection-status.tsx b/src/components/navbar/connection-status.tsx new file mode 100644 index 0000000..92a5938 --- /dev/null +++ b/src/components/navbar/connection-status.tsx @@ -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(undefined); + const clearProgressHandler: any = useRef(undefined); + const [lastUploadAt, setLastUploadAt] = useState(() => { + 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 ( +
+ + +
+ + { + syncStatus.connected ? + : + + + } + { + syncStatus.connected ? "Online" : "Offline" + } + + + + + + { + downloadProgress !== undefined ? + <> + + Saving ... + + + + + + : + + Saved {deltaUploadTime} + + + } +
+ +
+ + Last synced {syncStatus.lastSyncedAt?.toISOString()} + +
+
+ ) +} + + + +export default ConnectionStatus; \ No newline at end of file diff --git a/src/components/navbar/navbar.tsx b/src/components/navbar/navbar.tsx index e90c7a4..b6ed6ad 100644 --- a/src/components/navbar/navbar.tsx +++ b/src/components/navbar/navbar.tsx @@ -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 = ({ }) => { - const {resolvedTheme} = useTheme() ; + const { resolvedTheme } = useTheme(); return ( @@ -21,9 +22,13 @@ const Navbar: React.FC = ({ }) => { />

StackRender

-
- - +
+
+ + + + +
diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index c27e2cf..5868e56 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -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 = () => { - 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 = () => { } setIsConnectionInProgress(false); - }, [database , t ]); + }, [database, t]); // Called when nodes are updated (position changes or removed) const handleNodesChanges: OnNodesChange = useCallback(async (changes: NodeChange[]) => { @@ -156,9 +156,10 @@ const DatabasePage: React.FC = () => { return ( -
- - +
+
+ +
= () => { /> - +
; +interface Props { -interface Props { - } -const DBController : React.FC = ({ }) => { - console.log ("re-render db controller") - return( -
- -
+const DBController: React.FC = ({ }) => { + const [width, setWidth] = useState(512); + const onResize = (event: any, params: any) => { + setWidth(params.size.width); + } + + + return ( + +
+ } + > +
+ +
+ + ) } diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx index a887b70..a539ffd 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx @@ -21,7 +21,7 @@ const RelationshipAccordionBody: React.FC = ({ r const { t } = useTranslation(); - + const changeCardinality = (keys: SharedSelection) => { @@ -32,18 +32,29 @@ const RelationshipAccordionBody: React.FC = ({ 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 (
@@ -92,6 +103,7 @@ const RelationshipAccordionBody: React.FC = ({ r className="w-full" size="sm" variant="bordered" + label="cardinality" selectedKeys={cardinality} onSelectionChange={changeCardinality} diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx index 8a9a4c6..3f4bf1e 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx @@ -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 = } const onDeleteRelationship = () => { - deleteRelationship(relationship.id); setPopOverOpen(false); } - useEffect(() => { setName(relationship.name ? relationship.name : defaultName); - }, [relationship.name]) + }, [relationship.name]); + + return (
= } -export default RelationshipAccordionHeader; \ No newline at end of file +export default React.memo(RelationshipAccordionHeader, (previousState, newState) => { + return hash(previousState) == hash(newState); +}); \ No newline at end of file diff --git a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx index ca67c27..882f8c7 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx @@ -25,7 +25,7 @@ const RelationshipController: React.FC = ({ }) => { const [isValid, setIsValid] = useState(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 = ({ }) => { setSelectedRelationship(new Set([focusedRelationshipId]) as any); } - }, [focusedRelationshipId]) + }, [focusedRelationshipId]) ; return (
diff --git a/src/providers/sync-provider/sync-provider.tsx b/src/providers/sync-provider/sync-provider.tsx index ddf8a47..193cc64 100644 --- a/src/providers/sync-provider/sync-provider.tsx +++ b/src/providers/sync-provider/sync-provider.tsx @@ -35,8 +35,12 @@ export const SyncProvider: React.FC = ({ children }) => { useEffect(() => { powerSync.init(); powerSync.connect(connector); + + }, [powerSync, connector]) + + return ( }> diff --git a/src/utils/stackrender-connector.ts b/src/utils/stackrender-connector.ts index 231b227..660e06c 100644 --- a/src/utils/stackrender-connector.ts +++ b/src/utils/stackrender-connector.ts @@ -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;