From 8c0e989d4f6eabc18b7dc7271bb6b868e3f61114 Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 11 Oct 2024 18:44:27 +0800 Subject: [PATCH 1/2] feat: Render database tree #1841 --- gui/app/(dashboard)/actions.ts | 12 +- gui/app/(dashboard)/database/layout.tsx | 13 +- gui/app/(dashboard)/database/page.tsx | 2 +- gui/app/(dashboard)/database/styles.css | 47 ++++ gui/app/(dashboard)/database/tree.tsx | 306 ++++++++++++++++++++++++ gui/lib/request.ts | 2 +- gui/package.json | 3 + gui/pnpm-lock.yaml | 38 +++ gui/public/column.svg | 6 + gui/public/index.svg | 6 + gui/public/segment.svg | 6 + 11 files changed, 424 insertions(+), 17 deletions(-) create mode 100644 gui/app/(dashboard)/database/styles.css create mode 100644 gui/app/(dashboard)/database/tree.tsx create mode 100644 gui/public/column.svg create mode 100644 gui/public/index.svg create mode 100644 gui/public/segment.svg diff --git a/gui/app/(dashboard)/actions.ts b/gui/app/(dashboard)/actions.ts index 88ddaa5a1f..735bb8b413 100644 --- a/gui/app/(dashboard)/actions.ts +++ b/gui/app/(dashboard)/actions.ts @@ -133,9 +133,7 @@ export const showTable = async ({ export const showConfigs = async () => { try { - const x = await get( - `${ApiUrl.configs}` - ); + const x = await get(`${ApiUrl.configs}`); return x; } catch (error) { console.log('🚀 ~ error:', error); @@ -144,9 +142,7 @@ export const showConfigs = async () => { export const showVariables = async () => { try { - const x = await get( - `${ApiUrl.variables}/global` - ); + const x = await get(`${ApiUrl.variables}/global`); return x; } catch (error) { console.log('🚀 ~ error:', error); @@ -155,9 +151,7 @@ export const showVariables = async () => { export const showCurrentNode = async () => { try { - const x = await get( - `${ApiUrl.variables}/global` - ); + const x = await get(`${ApiUrl.variables}/global`); return x; } catch (error) { console.log('🚀 ~ error:', error); diff --git a/gui/app/(dashboard)/database/layout.tsx b/gui/app/(dashboard)/database/layout.tsx index f382f067cd..4dcd72f938 100644 --- a/gui/app/(dashboard)/database/layout.tsx +++ b/gui/app/(dashboard)/database/layout.tsx @@ -1,4 +1,4 @@ -import SideMenu, { MenuItem } from '@/components/ui/side-menu'; +import { MenuItem } from '@/components/ui/side-menu'; import { Table, TableBody, @@ -8,7 +8,7 @@ import { TableRow } from '@/components/ui/table'; import { listDatabase, listTable } from '../actions'; -import { InfinityContextMenuContent } from '../tables/context-menu'; +import AsyncTree from './tree'; async function InfinityTable() { const tables = await listTable('default_db'); @@ -70,16 +70,17 @@ export default async function DatabaseLayout({ } return ( -
-
- +
+ {/* ( )} - > + > */} +
{children}
diff --git a/gui/app/(dashboard)/database/page.tsx b/gui/app/(dashboard)/database/page.tsx index 3f9e71d12e..ccb97a3ed5 100644 --- a/gui/app/(dashboard)/database/page.tsx +++ b/gui/app/(dashboard)/database/page.tsx @@ -1,3 +1,3 @@ export default async function DatabasePage() { - return
DatabasePage
; + return
DatabasePage
; } diff --git a/gui/app/(dashboard)/database/styles.css b/gui/app/(dashboard)/database/styles.css new file mode 100644 index 0000000000..5ec9bedc21 --- /dev/null +++ b/gui/app/(dashboard)/database/styles.css @@ -0,0 +1,47 @@ +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loading-icon { + animation: spinner 1.5s linear infinite; + margin-left: 5px; +} + +.visually-hidden { + position: absolute; + clip-path: circle(0); + border: 0; + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + width: 1px; + white-space: nowrap; +} + +.tree-node { + display: flex; + align-items: center; + cursor: pointer; +} + +.tree-node:hover { + background: rgba(255, 255, 255, 0.1); +} + +.tree-node--focused { + background-color: #d7d7d7; +} + +.arrow--open { + transform: rotate(90deg); +} + +.name { + margin-left: 6px; +} diff --git a/gui/app/(dashboard)/database/tree.tsx b/gui/app/(dashboard)/database/tree.tsx new file mode 100644 index 0000000000..7be20530f0 --- /dev/null +++ b/gui/app/(dashboard)/database/tree.tsx @@ -0,0 +1,306 @@ +'use client'; + +import cx from 'classnames'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import TreeView from 'react-accessible-treeview'; +import { AiOutlineLoading } from 'react-icons/ai'; +import { IoMdArrowDropright } from 'react-icons/io'; +import ColumnIcon from '/public/column.svg'; +import DatabaseIcon from '/public/database.svg'; +import IndexIcon from '/public/index.svg'; +import SegmentIcon from '/public/segment.svg'; +import TableIcon from '/public/table.svg'; + +import { listDatabase, listTable } from '../actions'; +import './styles.css'; + +interface TreeNode { + name: string; + id: string | number; + children: Array; + parent: string | number | null; + isBranch?: boolean; +} + +const initialData = [ + { + name: '', + id: 0, + children: [1, 2, 3], + parent: null + }, + { + name: 'Fruits', + children: [], + id: 1, + parent: 0, + isBranch: true + }, + { + name: 'Drinks', + children: [4, 5], + id: 2, + parent: 0, + isBranch: true + }, + { + name: 'Vegetables', + children: [], + id: 3, + parent: 0, + isBranch: true + }, + { + name: 'Pine colada', + children: [], + id: 4, + parent: 2 + }, + { + name: 'Water', + children: [], + id: 5, + parent: 2 + } +]; + +enum Leaf { + Columns = 'Columns', + Index = 'Index', + Segments = 'Segments' +} + +const buildLeafData = (parent: string) => { + return [ + { + name: Leaf.Columns, + children: [], + id: `${Leaf.Columns}-${parent}`, + parent + }, + { name: Leaf.Index, children: [], id: `${Leaf.Index}-${parent}`, parent }, + { + name: Leaf.Segments, + children: [], + id: `${Leaf.Segments}-${parent}`, + parent + } + ]; +}; + +const LeafIconMap = { + [Leaf.Columns]: , + [Leaf.Index]: , + [Leaf.Segments]: +}; + +const renderIcon = (level: number, name: string) => { + if (level === 1) { + return ; + } else if (level === 2) { + return ; + } else { + return LeafIconMap[name as Leaf]; + } + return <>; +}; + +function AsyncTree() { + const loadedAlertElement = useRef(null); + const [data, setData] = useState(initialData); + const [nodesAlreadyLoaded, setNodesAlreadyLoaded] = useState([]); + + const updateTreeData = ( + list: any[], + id: string | number, + children: Array + ) => { + const data = list.map((node) => { + if (node.id === id) { + node.children = children.map((el) => { + return el.id; + }); + } + return node; + }); + return data.concat(children); + }; + + const fetchDatabases = useCallback(async () => { + const ret = await listDatabase(); + if (ret.databases.length > 0) { + setData((value) => + updateTreeData( + value, + 0, + ret.databases.map((x: string) => ({ + name: x, + children: [], + id: x, + parent: 0, + isBranch: true + })) + ) + ); + } + }, []); + + const fetchTables = useCallback(async (databaseName: string) => { + const ret = await listTable(databaseName); + if (ret?.tables?.length > 0) { + setData((value) => { + const tablePropertyList: TreeNode[] = []; + const tableList = ret.tables.map((x: string) => { + const leafs = buildLeafData(x); + tablePropertyList.push(...leafs); + + return { + name: x, + children: leafs.map((x) => x.id), + id: x, + parent: databaseName, + isBranch: true + }; + }); + + return [ + ...updateTreeData(value, databaseName, tableList), + ...tablePropertyList + ]; + }); + } + }, []); + + useEffect(() => { + fetchDatabases(); + }, [fetchDatabases]); + + const onLoadData = async ({ element }: { element: TreeNode }) => { + if (element.children.length > 0) { + return; + } + + await fetchTables(element.id as string); + + return undefined; + // return new Promise((resolve) => { + // setTimeout(() => { + // setData((value) => + // updateTreeData(value, element.id, [ + // { + // name: `Child Node ${value.length}`, + // children: [], + // id: value.length, + // parent: element.id, + // isBranch: true + // }, + // { + // name: 'Another child Node', + // children: [], + // id: value.length + 1, + // parent: element.id + // } + // ]) + // ); + // resolve(undefined); + // }, 1000); + // }); + }; + + const wrappedOnLoadData = async (props: any) => { + const nodeHasNoChildData = props.element.children.length === 0; + const nodeHasAlreadyBeenLoaded = nodesAlreadyLoaded.find( + (e) => e.id === props.element.id + ); + + await onLoadData(props); + + if (nodeHasNoChildData && !nodeHasAlreadyBeenLoaded) { + const el: any = loadedAlertElement.current; + setNodesAlreadyLoaded([...nodesAlreadyLoaded, props.element]); + el && (el.innerHTML = `${props.element.name} loaded`); + + // Clearing aria-live region so loaded node alerts no longer appear in DOM + setTimeout(() => { + el && (el.innerHTML = ''); + }, 5000); + } + }; + + return ( + <> +
+
+ { + const branchNode = (isExpanded: any, element: any) => { + return isExpanded && element.children.length === 0 ? ( + <> + + loading {element.name} + + + + ) : ( + + ); + }; + return ( +
+ {isBranch && branchNode(isExpanded, element)} +
+ {renderIcon(level, element.name)} + {element.name} +
+
+ ); + }} + /> +
+ + ); +} + +const ArrowIcon = ({ isOpen, className }: any) => { + const baseClass = 'arrow'; + const classes = cx( + baseClass, + { [`${baseClass}--closed`]: !isOpen }, + { [`${baseClass}--open`]: isOpen }, + className + ); + return ; +}; + +export default AsyncTree; diff --git a/gui/lib/request.ts b/gui/lib/request.ts index 351a06eafc..fe2ca7d95b 100644 --- a/gui/lib/request.ts +++ b/gui/lib/request.ts @@ -1,4 +1,4 @@ -const baseUrl = 'http://127.0.0.1:3000/'; +const baseUrl = 'http://127.0.0.1:23820/'; export const request = async ( url: string, diff --git a/gui/package.json b/gui/package.json index 71c004e06d..f39784b678 100644 --- a/gui/package.json +++ b/gui/package.json @@ -25,6 +25,7 @@ "@vercel/analytics": "^1.3.1", "autoprefixer": "^10.4.19", "class-variance-authority": "^0.7.0", + "classnames": "^2.5.1", "clsx": "^2.1.1", "drizzle-kit": "^0.22.8", "drizzle-orm": "^0.31.2", @@ -36,8 +37,10 @@ "prettier": "^3.3.2", "prop-types": "^15.8.1", "react": "19.0.0-rc.0", + "react-accessible-treeview": "^2.9.1", "react-dom": "19.0.0-rc.0", "react-hook-form": "^7.53.0", + "react-icons": "^5.3.0", "server-only": "^0.0.1", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.4", diff --git a/gui/pnpm-lock.yaml b/gui/pnpm-lock.yaml index 9394675a0b..b6b5e1b04b 100644 --- a/gui/pnpm-lock.yaml +++ b/gui/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: class-variance-authority: specifier: ^0.7.0 version: 0.7.0 + classnames: + specifier: ^2.5.1 + version: 2.5.1 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -95,12 +98,18 @@ importers: react: specifier: 19.0.0-rc.0 version: 19.0.0-rc.0 + react-accessible-treeview: + specifier: ^2.9.1 + version: 2.9.1(classnames@2.5.1)(prop-types@15.8.1)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) react-dom: specifier: 19.0.0-rc.0 version: 19.0.0-rc.0(react@19.0.0-rc.0) react-hook-form: specifier: ^7.53.0 version: 7.53.0(react@19.0.0-rc.0) + react-icons: + specifier: ^5.3.0 + version: 5.3.0(react@19.0.0-rc.0) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -1956,6 +1965,9 @@ packages: class-variance-authority@0.7.0: resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -2702,6 +2714,14 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-accessible-treeview@2.9.1: + resolution: {integrity: sha512-UlmeFJtlh1SryN8kHLf4ICLlF4HeIpiacWwHh+7AzhmOmOs9vObXAjJWTiLpxP342TQ1QbCdSGq086Y8oD7NSw==} + peerDependencies: + classnames: ^2.2.6 + prop-types: ^15.7.2 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom@19.0.0-rc.0: resolution: {integrity: sha512-MhgN2RMYFUkZekkFbsXg9ycwEGaMBzATpTNvGGvWNA9BZZEkdzIL4pv7iDuZKn48YoGARk8ydu4S+Ehd8Yrc4g==} peerDependencies: @@ -2713,6 +2733,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-icons@5.3.0: + resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4878,6 +4903,8 @@ snapshots: dependencies: clsx: 2.0.0 + classnames@2.5.1: {} + client-only@0.0.1: {} clsx@2.0.0: {} @@ -5508,6 +5535,13 @@ snapshots: queue-microtask@1.2.3: {} + react-accessible-treeview@2.9.1(classnames@2.5.1)(prop-types@15.8.1)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0): + dependencies: + classnames: 2.5.1 + prop-types: 15.8.1 + react: 19.0.0-rc.0 + react-dom: 19.0.0-rc.0(react@19.0.0-rc.0) + react-dom@19.0.0-rc.0(react@19.0.0-rc.0): dependencies: react: 19.0.0-rc.0 @@ -5517,6 +5551,10 @@ snapshots: dependencies: react: 19.0.0-rc.0 + react-icons@5.3.0(react@19.0.0-rc.0): + dependencies: + react: 19.0.0-rc.0 + react-is@16.13.1: {} react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@19.0.0-rc.0): diff --git a/gui/public/column.svg b/gui/public/column.svg new file mode 100644 index 0000000000..58e54a3a59 --- /dev/null +++ b/gui/public/column.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/gui/public/index.svg b/gui/public/index.svg new file mode 100644 index 0000000000..73dba7b77e --- /dev/null +++ b/gui/public/index.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/gui/public/segment.svg b/gui/public/segment.svg new file mode 100644 index 0000000000..d3c9493aa3 --- /dev/null +++ b/gui/public/segment.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file From fa84fc56b317be59b1834684d824daf1dd471132 Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 11 Oct 2024 19:30:37 +0800 Subject: [PATCH 2/2] feat: Add constants.ts to database folder --- gui/app/(dashboard)/database/constants.tsx | 57 ++++++ gui/app/(dashboard)/database/hooks.ts | 144 ++++++++++++++ gui/app/(dashboard)/database/interface.ts | 9 + gui/app/(dashboard)/database/tree.tsx | 219 ++------------------- gui/app/(dashboard)/database/utils.ts | 40 ++++ 5 files changed, 263 insertions(+), 206 deletions(-) create mode 100644 gui/app/(dashboard)/database/constants.tsx create mode 100644 gui/app/(dashboard)/database/hooks.ts create mode 100644 gui/app/(dashboard)/database/interface.ts create mode 100644 gui/app/(dashboard)/database/utils.ts diff --git a/gui/app/(dashboard)/database/constants.tsx b/gui/app/(dashboard)/database/constants.tsx new file mode 100644 index 0000000000..70962da090 --- /dev/null +++ b/gui/app/(dashboard)/database/constants.tsx @@ -0,0 +1,57 @@ +import ColumnIcon from '/public/column.svg'; +import IndexIcon from '/public/index.svg'; +import SegmentIcon from '/public/segment.svg'; + +export enum Leaf { + Columns = 'Columns', + Index = 'Index', + Segments = 'Segments' +} + +export const LeafIconMap = { + [Leaf.Columns]: , + [Leaf.Index]: , + [Leaf.Segments]: +}; + +export const initialData = [ + { + name: '', + id: 0, + children: [1, 2, 3], + parent: null + }, + { + name: 'Fruits', + children: [], + id: 1, + parent: 0, + isBranch: true + }, + { + name: 'Drinks', + children: [4, 5], + id: 2, + parent: 0, + isBranch: true + }, + { + name: 'Vegetables', + children: [], + id: 3, + parent: 0, + isBranch: true + }, + { + name: 'Pine colada', + children: [], + id: 4, + parent: 2 + }, + { + name: 'Water', + children: [], + id: 5, + parent: 2 + } +]; diff --git a/gui/app/(dashboard)/database/hooks.ts b/gui/app/(dashboard)/database/hooks.ts new file mode 100644 index 0000000000..c3e26010ab --- /dev/null +++ b/gui/app/(dashboard)/database/hooks.ts @@ -0,0 +1,144 @@ +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { listDatabase, listTable } from '../actions'; +import { initialData } from './constants'; +import { TreeNode, TreeParentId } from './interface'; +import { buildLeafData, getParentIdById, updateTreeData } from './utils'; + +export const useHandleClickTreeName = () => { + const router = useRouter(); + + const handleClickTreeName = useCallback( + ({ + level, + name, + parent, + data + }: { + level: number; + name: string; + parent: TreeParentId; + data: TreeNode[]; + }) => + () => { + if (level === 3) { + const databaseId = getParentIdById(data, parent); + if (databaseId) { + router.push(`/database/${databaseId}/table/${parent}?tab=${name}`); + } + } + }, + [] + ); + + return { handleClickTreeName }; +}; + +export const useBuildTreeData = () => { + const loadedAlertElement = useRef(null); + const [data, setData] = useState(initialData); + const [nodesAlreadyLoaded, setNodesAlreadyLoaded] = useState([]); + + const fetchDatabases = useCallback(async () => { + const ret = await listDatabase(); + if (ret.databases.length > 0) { + setData((value) => + updateTreeData( + value, + 0, + ret.databases.map((x: string) => ({ + name: x, + children: [], + id: x, + parent: 0, + isBranch: true + })) + ) + ); + } + }, []); + + const fetchTables = useCallback(async (databaseName: string) => { + const ret = await listTable(databaseName); + if (ret?.tables?.length > 0) { + setData((value) => { + const tablePropertyList: TreeNode[] = []; + const tableList = ret.tables.map((x: string) => { + const leafs = buildLeafData(x); + tablePropertyList.push(...leafs); + + return { + name: x, + children: leafs.map((x) => x.id), + id: x, + parent: databaseName, + isBranch: true + }; + }); + + return [ + ...updateTreeData(value, databaseName, tableList), + ...tablePropertyList + ]; + }); + } + }, []); + + useEffect(() => { + fetchDatabases(); + }, [fetchDatabases]); + + const onLoadData = async ({ element }: { element: TreeNode }) => { + if (element.children.length > 0) { + return; + } + + await fetchTables(element.id as string); + + return undefined; + // return new Promise((resolve) => { + // setTimeout(() => { + // setData((value) => + // updateTreeData(value, element.id, [ + // { + // name: `Child Node ${value.length}`, + // children: [], + // id: value.length, + // parent: element.id, + // isBranch: true + // }, + // { + // name: 'Another child Node', + // children: [], + // id: value.length + 1, + // parent: element.id + // } + // ]) + // ); + // resolve(undefined); + // }, 1000); + // }); + }; + + const wrappedOnLoadData = async (props: any) => { + const nodeHasNoChildData = props.element.children.length === 0; + const nodeHasAlreadyBeenLoaded = nodesAlreadyLoaded.find( + (e) => e.id === props.element.id + ); + + await onLoadData(props); + + if (nodeHasNoChildData && !nodeHasAlreadyBeenLoaded) { + const el: any = loadedAlertElement.current; + setNodesAlreadyLoaded([...nodesAlreadyLoaded, props.element]); + el && (el.innerHTML = `${props.element.name} loaded`); + + // Clearing aria-live region so loaded node alerts no longer appear in DOM + setTimeout(() => { + el && (el.innerHTML = ''); + }, 5000); + } + }; + + return { wrappedOnLoadData, data, loadedAlertElement }; +}; diff --git a/gui/app/(dashboard)/database/interface.ts b/gui/app/(dashboard)/database/interface.ts new file mode 100644 index 0000000000..dea270cd04 --- /dev/null +++ b/gui/app/(dashboard)/database/interface.ts @@ -0,0 +1,9 @@ +export type TreeParentId = string | number | null; + +export interface TreeNode { + name: string; + id: string | number; + children: Array; + parent: TreeParentId; + isBranch?: boolean; +} diff --git a/gui/app/(dashboard)/database/tree.tsx b/gui/app/(dashboard)/database/tree.tsx index 7be20530f0..ef9df098e5 100644 --- a/gui/app/(dashboard)/database/tree.tsx +++ b/gui/app/(dashboard)/database/tree.tsx @@ -1,99 +1,16 @@ 'use client'; import cx from 'classnames'; -import { useCallback, useEffect, useRef, useState } from 'react'; import TreeView from 'react-accessible-treeview'; import { AiOutlineLoading } from 'react-icons/ai'; import { IoMdArrowDropright } from 'react-icons/io'; -import ColumnIcon from '/public/column.svg'; +import { Leaf, LeafIconMap } from './constants'; import DatabaseIcon from '/public/database.svg'; -import IndexIcon from '/public/index.svg'; -import SegmentIcon from '/public/segment.svg'; import TableIcon from '/public/table.svg'; -import { listDatabase, listTable } from '../actions'; +import { useBuildTreeData, useHandleClickTreeName } from './hooks'; import './styles.css'; -interface TreeNode { - name: string; - id: string | number; - children: Array; - parent: string | number | null; - isBranch?: boolean; -} - -const initialData = [ - { - name: '', - id: 0, - children: [1, 2, 3], - parent: null - }, - { - name: 'Fruits', - children: [], - id: 1, - parent: 0, - isBranch: true - }, - { - name: 'Drinks', - children: [4, 5], - id: 2, - parent: 0, - isBranch: true - }, - { - name: 'Vegetables', - children: [], - id: 3, - parent: 0, - isBranch: true - }, - { - name: 'Pine colada', - children: [], - id: 4, - parent: 2 - }, - { - name: 'Water', - children: [], - id: 5, - parent: 2 - } -]; - -enum Leaf { - Columns = 'Columns', - Index = 'Index', - Segments = 'Segments' -} - -const buildLeafData = (parent: string) => { - return [ - { - name: Leaf.Columns, - children: [], - id: `${Leaf.Columns}-${parent}`, - parent - }, - { name: Leaf.Index, children: [], id: `${Leaf.Index}-${parent}`, parent }, - { - name: Leaf.Segments, - children: [], - id: `${Leaf.Segments}-${parent}`, - parent - } - ]; -}; - -const LeafIconMap = { - [Leaf.Columns]: , - [Leaf.Index]: , - [Leaf.Segments]: -}; - const renderIcon = (level: number, name: string) => { if (level === 1) { return ; @@ -106,126 +23,8 @@ const renderIcon = (level: number, name: string) => { }; function AsyncTree() { - const loadedAlertElement = useRef(null); - const [data, setData] = useState(initialData); - const [nodesAlreadyLoaded, setNodesAlreadyLoaded] = useState([]); - - const updateTreeData = ( - list: any[], - id: string | number, - children: Array - ) => { - const data = list.map((node) => { - if (node.id === id) { - node.children = children.map((el) => { - return el.id; - }); - } - return node; - }); - return data.concat(children); - }; - - const fetchDatabases = useCallback(async () => { - const ret = await listDatabase(); - if (ret.databases.length > 0) { - setData((value) => - updateTreeData( - value, - 0, - ret.databases.map((x: string) => ({ - name: x, - children: [], - id: x, - parent: 0, - isBranch: true - })) - ) - ); - } - }, []); - - const fetchTables = useCallback(async (databaseName: string) => { - const ret = await listTable(databaseName); - if (ret?.tables?.length > 0) { - setData((value) => { - const tablePropertyList: TreeNode[] = []; - const tableList = ret.tables.map((x: string) => { - const leafs = buildLeafData(x); - tablePropertyList.push(...leafs); - - return { - name: x, - children: leafs.map((x) => x.id), - id: x, - parent: databaseName, - isBranch: true - }; - }); - - return [ - ...updateTreeData(value, databaseName, tableList), - ...tablePropertyList - ]; - }); - } - }, []); - - useEffect(() => { - fetchDatabases(); - }, [fetchDatabases]); - - const onLoadData = async ({ element }: { element: TreeNode }) => { - if (element.children.length > 0) { - return; - } - - await fetchTables(element.id as string); - - return undefined; - // return new Promise((resolve) => { - // setTimeout(() => { - // setData((value) => - // updateTreeData(value, element.id, [ - // { - // name: `Child Node ${value.length}`, - // children: [], - // id: value.length, - // parent: element.id, - // isBranch: true - // }, - // { - // name: 'Another child Node', - // children: [], - // id: value.length + 1, - // parent: element.id - // } - // ]) - // ); - // resolve(undefined); - // }, 1000); - // }); - }; - - const wrappedOnLoadData = async (props: any) => { - const nodeHasNoChildData = props.element.children.length === 0; - const nodeHasAlreadyBeenLoaded = nodesAlreadyLoaded.find( - (e) => e.id === props.element.id - ); - - await onLoadData(props); - - if (nodeHasNoChildData && !nodeHasAlreadyBeenLoaded) { - const el: any = loadedAlertElement.current; - setNodesAlreadyLoaded([...nodesAlreadyLoaded, props.element]); - el && (el.innerHTML = `${props.element.name} loaded`); - - // Clearing aria-live region so loaded node alerts no longer appear in DOM - setTimeout(() => { - el && (el.innerHTML = ''); - }, 5000); - } - }; + const { data, loadedAlertElement, wrappedOnLoadData } = useBuildTreeData(); + const { handleClickTreeName } = useHandleClickTreeName(); return ( <> @@ -279,7 +78,15 @@ function AsyncTree() { style={{ marginLeft: 40 * (level - 1) }} > {isBranch && branchNode(isExpanded, element)} -
+
{renderIcon(level, element.name)} {element.name}
diff --git a/gui/app/(dashboard)/database/utils.ts b/gui/app/(dashboard)/database/utils.ts new file mode 100644 index 0000000000..0c54a73169 --- /dev/null +++ b/gui/app/(dashboard)/database/utils.ts @@ -0,0 +1,40 @@ +import { Leaf } from './constants'; +import { TreeNode, TreeParentId } from './interface'; + +export const updateTreeData = ( + list: any[], + id: string | number, + children: Array +) => { + const data = list.map((node) => { + if (node.id === id) { + node.children = children.map((el) => { + return el.id; + }); + } + return node; + }); + return data.concat(children); +}; + +export const buildLeafData = (parent: string) => { + return [ + { + name: Leaf.Columns, + children: [], + id: `${Leaf.Columns}-${parent}`, + parent + }, + { name: Leaf.Index, children: [], id: `${Leaf.Index}-${parent}`, parent }, + { + name: Leaf.Segments, + children: [], + id: `${Leaf.Segments}-${parent}`, + parent + } + ]; +}; + +export const getParentIdById = (data: TreeNode[], id: TreeParentId) => { + return data.find((x) => x.id === id)?.parent; +};