mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
open database feature is done
This commit is contained in:
@@ -4,12 +4,12 @@ import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
import { getDefaultTableOverlapping } from "@/utils/tables";
|
||||
import hash from "object-hash";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
|
||||
|
||||
export const useTableToNode = (tables: TableType[]): void => {
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const tableNodes = tables.map((table: TableType) => {
|
||||
@@ -33,7 +33,7 @@ export const useTableToNode = (tables: TableType[]): void => {
|
||||
})
|
||||
|
||||
setNodes((nodes) => {
|
||||
|
||||
|
||||
return tableNodes.map((tableNode) => {
|
||||
const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id);
|
||||
if (!node)
|
||||
|
||||
+1
-9
@@ -1,34 +1,26 @@
|
||||
import { en, enLanguage } from "./languages/en";
|
||||
import { fr, frLanguage } from "./languages/fr";
|
||||
import { Language } from "./types";
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { ar, arLanguage } from "./languages/ar";
|
||||
|
||||
|
||||
|
||||
export const languages: Language[] = [
|
||||
enLanguage,
|
||||
frLanguage ,
|
||||
arLanguage
|
||||
];
|
||||
|
||||
|
||||
const resources = {
|
||||
en,
|
||||
fr,
|
||||
ar
|
||||
};
|
||||
|
||||
|
||||
|
||||
i18n.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources ,
|
||||
lng: 'fr',
|
||||
lng: 'en',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@xyflow/react";
|
||||
|
||||
// React built-ins
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
// Custom components and styles
|
||||
import Table from "./table/table";
|
||||
@@ -55,7 +55,7 @@ const DatabasePage: React.FC = () => {
|
||||
const { setIsConnectionInProgress } = useDiagramOps();
|
||||
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database;
|
||||
const { tables, relationships } = database;
|
||||
|
||||
// Hook to allow zooming and centering the diagram
|
||||
const { fitView } = useReactFlow();
|
||||
@@ -63,7 +63,7 @@ const DatabasePage: React.FC = () => {
|
||||
// Define custom node and edge types
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||
|
||||
|
||||
// Called when a connection is made between fields
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop();
|
||||
@@ -113,12 +113,11 @@ const DatabasePage: React.FC = () => {
|
||||
} as TableInsertType)));
|
||||
|
||||
// Delete tables if removed
|
||||
if (nodeRemoveChanges.length > 0) {
|
||||
|
||||
if (nodeRemoveChanges.length > 0 ) {
|
||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||
}
|
||||
return onNodesChange(changes);
|
||||
}, [onNodesChange]);
|
||||
}, [onNodesChange ]);
|
||||
|
||||
// Called when edges (relationships) change
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
|
||||
|
||||
interface Props { }
|
||||
@@ -45,6 +46,7 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
name: `table_${tables.length + 1}`,
|
||||
posX,
|
||||
posY,
|
||||
|
||||
fields: [{
|
||||
id: v4(),
|
||||
name: "id",
|
||||
|
||||
@@ -14,7 +14,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
|
||||
const { t } = useTranslation();
|
||||
const [selectedDbType, setSelectedDbType] = useState<string[]>([DBTypes[0].dialect]);
|
||||
const [dbName, setDbName] = useState<string>("db_example");
|
||||
const { createDatabase, setCurrentDatabaseId } = useDatabaseOperations();
|
||||
const { createDatabase, switchDatabase } = useDatabaseOperations();
|
||||
|
||||
const onDatabaseTypeChange = (types: string[]) => {
|
||||
const selectedType: string | undefined = types.pop();
|
||||
@@ -31,7 +31,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
|
||||
name: dbName,
|
||||
dialect: selectedDbType[0] as any
|
||||
});
|
||||
setCurrentDatabaseId(databaseId);
|
||||
switchDatabase(databaseId);
|
||||
res(databaseId)
|
||||
})
|
||||
|
||||
|
||||
@@ -15,15 +15,16 @@ import { useTranslation } from "react-i18next";
|
||||
const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { databases } = useDatabase();
|
||||
const { setCurrentDatabaseId } = useDatabaseOperations();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(undefined);
|
||||
const { databases , currentDatabaseId} = useDatabase();
|
||||
const { switchDatabase } = useDatabaseOperations();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(
|
||||
(() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined)
|
||||
);
|
||||
|
||||
const openDatabase = () => {
|
||||
setCurrentDatabaseId( selectedDatabase.currentKey)
|
||||
switchDatabase( selectedDatabase.currentKey)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -44,11 +45,13 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
selectedKeys={selectedDatabase}
|
||||
onSelectionChange={setSelectedDatabase}
|
||||
classNames={{
|
||||
wrapper: "min-h-[360px] shadow-none border-1 border-divider border-sm"
|
||||
wrapper: "min-h-[360px] shadow-none border-1 border-divider rounded-sm" ,
|
||||
th : "dark:bg-background" ,
|
||||
tr : "hover:bg-default-100 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 "
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn>Dialect</TableColumn>
|
||||
<TableHeader className="rounded-sm">
|
||||
<TableColumn >Dialect</TableColumn>
|
||||
<TableColumn>Name</TableColumn>
|
||||
<TableColumn>Created at</TableColumn>
|
||||
<TableColumn>Tables</TableColumn>
|
||||
@@ -63,7 +66,7 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
width={24}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{database.name}</TableCell>
|
||||
<TableCell className="font-semibold">{database.name}</TableCell>
|
||||
<TableCell>{
|
||||
new Date(database.createdAt as string).toLocaleString("en-US")
|
||||
}</TableCell>
|
||||
|
||||
@@ -15,8 +15,10 @@ import { createContext } from "react";
|
||||
interface DatabaseDataContextType {
|
||||
|
||||
database: DatabaseType,
|
||||
databases : DatabaseType[] ,
|
||||
currentDatabaseId: string | undefined,
|
||||
databases: DatabaseType[],
|
||||
isLoading: boolean,
|
||||
isSwitchingDatabase: boolean,
|
||||
getField: (tableId: string, id: string) => FieldType | undefined,
|
||||
|
||||
}
|
||||
@@ -27,14 +29,14 @@ interface DatabaseOperationsContextType {
|
||||
// database operations
|
||||
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
|
||||
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
|
||||
deleteDatabase : ( id : string) => Promise<void> ,
|
||||
setCurrentDatabaseId : ( id : string) => void ,
|
||||
deleteDatabase: (id: string) => Promise<void>,
|
||||
switchDatabase: (databaseId: string) => void,
|
||||
|
||||
createTable: (table: TableInsertType) => Promise<void>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<void>,
|
||||
updateTablePositions: (tables: TableInsertType[]) => Promise<void>,
|
||||
deleteMultiTables: (ids: string[]) => Promise<QueryResult>
|
||||
deleteMultiTables: (ids: string[]) => Promise<void>
|
||||
// field operations
|
||||
createField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
editField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { db } from "../sync-provider/sync-provider";
|
||||
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useQuery } from "@powersync/react";
|
||||
import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
import { asc, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { asc, count, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import { QueryResult, Transaction } from "@powersync/web";
|
||||
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipInsertType, relationships } from "@/lib/schemas/relationship-schema";
|
||||
import DatabaseHistoryProvider from "../database-history/database-history-provider";
|
||||
@@ -15,6 +15,7 @@ import { getTimestamp } from "@/utils/utils";
|
||||
import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
@@ -23,17 +24,17 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(undefined);
|
||||
|
||||
// Fetch all databases
|
||||
const { data: databases, isLoading: loadingDatabases , isFetching : fetchingDatabases} = useQuery(toCompilableQuery(
|
||||
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
|
||||
db.query.databases.findMany()
|
||||
));
|
||||
|
||||
// Fetch all data types
|
||||
const { data: data_types, isLoading: loadingDataTypes , isFetching : fetchingDataTypes} = useQuery(toCompilableQuery(
|
||||
const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany()
|
||||
));
|
||||
|
||||
// Fetch the current database with nested tables, fields, and relationships
|
||||
let { data: database, isLoading: loadingCurrentDatabase , isFetching : fetchingCurrentDatabase} = useQuery(
|
||||
let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery(
|
||||
toCompilableQuery(
|
||||
db.query.databases.findMany({
|
||||
where: (databases, { eq }) => eq(databases.id, currentDatabaseId as string),
|
||||
@@ -68,9 +69,13 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const switchDatabase = useCallback((databaseId: string) => {
|
||||
setCurrentDatabaseId(databaseId);
|
||||
localStorage.setItem("database_id", databaseId);
|
||||
}, [])
|
||||
|
||||
// Normalize result to single object
|
||||
if (database.length == 1)
|
||||
database = database[0] as any;
|
||||
@@ -78,11 +83,16 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
// Auto-select first database if none is selected
|
||||
useEffect(() => {
|
||||
if (databases.length > 0 && !currentDatabaseId) {
|
||||
setCurrentDatabaseId(databases[0].id);
|
||||
switchDatabase(databases[0].id);
|
||||
}
|
||||
}, [currentDatabaseId, databases])
|
||||
}, [currentDatabaseId, databases]);
|
||||
|
||||
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
|
||||
const isSwitchingDatabase: boolean = useMemo(() => {
|
||||
return isFetching && currentDatabaseId != (database as any)?.id
|
||||
}, [currentDatabaseId, database, isFetching]);
|
||||
|
||||
|
||||
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase ;
|
||||
// CRUD for database
|
||||
const createDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(databaseModel).values({
|
||||
@@ -99,6 +109,20 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
await db.delete(databaseModel).where(eq(databaseModel.id, id));
|
||||
}, [db]);
|
||||
|
||||
const updateDbNumTables = useCallback(async (databaseId: string, tx: any) => {
|
||||
|
||||
const [{ count : numOfTables }] = await tx
|
||||
.select({ count: count() })
|
||||
.from(tables)
|
||||
.where(eq(tables.databaseId, databaseId))
|
||||
|
||||
console.log (numOfTables)
|
||||
await tx.update(databaseModel).set({
|
||||
numOfTables
|
||||
}).where(eq(databaseModel.id, databaseId))
|
||||
}, []);
|
||||
|
||||
|
||||
// CRUD operations for Tables
|
||||
const createTable = useCallback(async (table: TableInsertType): Promise<void> => {
|
||||
if (currentDatabaseId) {
|
||||
@@ -108,6 +132,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
databaseId: currentDatabaseId,
|
||||
createdAt: table.createdAt || getTimestamp()
|
||||
});
|
||||
|
||||
|
||||
|
||||
await updateDbNumTables(currentDatabaseId, tx);
|
||||
if (table.fields) {
|
||||
await tx.insert(fields).values(
|
||||
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
|
||||
@@ -124,12 +152,28 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}, [db]);
|
||||
|
||||
const deleteTable = useCallback(async (id: string): Promise<void> => {
|
||||
await db.delete(tables).where(eq(tables.id, id));
|
||||
}, [db]);
|
||||
if (currentDatabaseId) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(tables).where(eq(tables.id, id));
|
||||
await updateDbNumTables(currentDatabaseId, tx)
|
||||
})
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
|
||||
const deleteMultiTables = useCallback(async (ids: string[]): Promise<QueryResult> => {
|
||||
return await db.delete(tables).where(inArray(tables.id, ids));
|
||||
}, [db]);
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
const deleteMultiTables = useCallback(async (ids: string[]): Promise<void> => {
|
||||
if (currentDatabaseId) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(tables).where(inArray(tables.id, ids));;
|
||||
await updateDbNumTables(currentDatabaseId, tx)
|
||||
})
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
// CRUD operations for Fields
|
||||
const createField = useCallback(async (field: FieldInsertType): Promise<QueryResult> => {
|
||||
@@ -191,7 +235,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
id: v4(),
|
||||
fieldId,
|
||||
indexId,
|
||||
|
||||
})))
|
||||
})
|
||||
}, [db])
|
||||
@@ -234,11 +277,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
// Apply a list of diff operations to sync database
|
||||
const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => {
|
||||
try {
|
||||
console.log ("excuted diff operations")
|
||||
await db.transaction(async (tx) => {
|
||||
for (const operation of operations) {
|
||||
if (operation.type == "CREATE_TABLE") {
|
||||
await tx.insert(tables).values(operation.table);
|
||||
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
|
||||
if (operation.table.fields && Object.values(operation.table.fields).length > 0)
|
||||
await tx.insert(fields).values(Object.values(operation.table.fields));
|
||||
|
||||
@@ -246,7 +289,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId));
|
||||
|
||||
} else if (operation.type === "DELETE_TABLE") {
|
||||
|
||||
await tx.delete(tables).where(eq(tables.id, operation.tableId));
|
||||
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
|
||||
|
||||
} else if (operation.type === "CREATE_FIELD") {
|
||||
await tx.insert(fields).values(operation.field);
|
||||
@@ -292,14 +337,14 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}, [db]);
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
const databaseOpsValue = useMemo(() => ({
|
||||
|
||||
createDatabase,
|
||||
editDatabase,
|
||||
deleteDatabase,
|
||||
setCurrentDatabaseId,
|
||||
switchDatabase,
|
||||
createTable,
|
||||
editTable,
|
||||
deleteTable,
|
||||
@@ -348,13 +393,17 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
<DatabaseDataContext.Provider value={{
|
||||
|
||||
database: database as unknown as DatabaseType,
|
||||
currentDatabaseId ,
|
||||
databases: databases as DatabaseType[],
|
||||
isLoading,
|
||||
isSwitchingDatabase,
|
||||
|
||||
|
||||
getField,
|
||||
}}>
|
||||
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
|
||||
{
|
||||
!isLoading && database &&
|
||||
database && !isLoading && !isSwitchingDatabase &&
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>
|
||||
|
||||
+47
-8
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
.react-flow__attribution {
|
||||
display: none;
|
||||
}
|
||||
@@ -121,17 +119,58 @@ div[data-slot="content"] hr[role="separator"] {
|
||||
}
|
||||
|
||||
svg.text-icon:hover {
|
||||
|
||||
color : #333639
|
||||
|
||||
color: #333639
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dark .bg-background-50 {
|
||||
background-color: red !important;
|
||||
.dark .bg-background-50 {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
|
||||
.database-checkbox[data-selected="true"]{
|
||||
border : 1.5px solid hsl(var(--heroui-primary-300));
|
||||
.database-checkbox[data-selected="true"] {
|
||||
border: 1.5px solid hsl(var(--heroui-primary-300));
|
||||
}
|
||||
|
||||
|
||||
|
||||
thead[role="rowgroup"] th {
|
||||
border-radius: 0px !important;
|
||||
|
||||
}
|
||||
|
||||
thead[role="rowgroup"] th:last-child {
|
||||
border-radius: 0px 4px 4px 0px !important;
|
||||
|
||||
}
|
||||
|
||||
thead[role="rowgroup"] th:first-child {
|
||||
border-radius: 4px 0px 0px 4px !important;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
tbody[role="rowgroup"] tr td::before {
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
tbody[role="rowgroup"] tr td {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
|
||||
tbody[role="rowgroup"] tr td:last-child {
|
||||
border-radius: 0px 4px 4px 0px !important;
|
||||
|
||||
}
|
||||
|
||||
tbody[role="rowgroup"] tr td:first-child {
|
||||
border-radius: 4px 0px 0px 4px !important;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user