diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx
new file mode 100644
index 0000000..e728b51
--- /dev/null
+++ b/web/src/components/tree.tsx
@@ -0,0 +1,138 @@
+import React, { useEffect, useState } from 'react'
+import { Menu, MenuItem } from '@mui/material'
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
+import ChevronRightIcon from '@mui/icons-material/ChevronRight'
+
+import { TreeNode } from '../utils/types'
+
+type TreeViewProps = {
+ node: TreeNode
+ selectedFilePath: string
+ handleSelect: (filePath: string) => void
+ defaultExpanded?: string[]
+}
+
+const TreeView = ({
+ node,
+ selectedFilePath,
+ handleSelect,
+ defaultExpanded
+}: TreeViewProps) => {
+ return (
+
+ )
+}
+
+export default TreeView
+
+type TreeViewNodeProps = {
+ node: TreeNode
+ selectedFilePath: string
+ handleSelect: (filePath: string) => void
+ defaultExpanded?: string[]
+}
+
+const TreeViewNode = ({
+ node,
+ selectedFilePath,
+ handleSelect,
+ defaultExpanded
+}: TreeViewNodeProps) => {
+ const [childVisible, setChildVisibility] = useState(false)
+ const [contextMenu, setContextMenu] = useState<{
+ mouseX: number
+ mouseY: number
+ } | null>(null)
+
+ const handleContextMenu = (event: React.MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ setContextMenu(
+ contextMenu === null
+ ? {
+ mouseX: event.clientX + 2,
+ mouseY: event.clientY - 6
+ }
+ : null
+ )
+ }
+
+ const hasChild = node.children.length ? true : false
+
+ const handleItemClick = () => {
+ if (node.children.length) {
+ setChildVisibility((v) => !v)
+ return
+ }
+
+ if (!node.name.includes('.')) return
+
+ handleSelect(node.relativePath)
+ }
+
+ useEffect(() => {
+ if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
+ setChildVisibility(true)
+ defaultExpanded.shift()
+ }
+ }, [defaultExpanded, node.relativePath])
+
+ return (
+
+
+ handleItemClick()}
+ >
+ {hasChild &&
+ (childVisible ?
:
)}
+
{node.name}
+
+
+ {hasChild &&
+ childVisible &&
+ node.children.map((child, index) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/web/src/index.css b/web/src/index.css
index 34d605c..6a506a5 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -25,3 +25,15 @@ code {
padding: '5px 10px';
margin-top: '10px';
}
+
+.tree-item-label {
+ display: flex;
+}
+
+.tree-item-label.selected {
+ background: lightgoldenrodyellow;
+}
+
+.tree-item-label:hover {
+ background: lightgray;
+}
diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts
index 4f0a80a..6d48d9b 100644
--- a/web/src/utils/types.ts
+++ b/web/src/utils/types.ts
@@ -30,3 +30,9 @@ export interface RegisterPermissionPayload {
principalType: string
principalId: number
}
+
+export interface TreeNode {
+ name: string
+ relativePath: string
+ children: Array
+}