mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Add network and syncing status and made the database controller resizebale
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+17
-5
@@ -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}
|
||||
|
||||
|
||||
+8
-6
@@ -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);
|
||||
});
|
||||
+2
-2
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user