FEAT: Table tree is now displayed!

Has no functionality yet, but that's next. And not sure how it works
when switching connections.
This commit is contained in:
Azpect3120 2024-08-07 18:08:40 -07:00
parent fb548e4207
commit 276a6be7b9
5 changed files with 203 additions and 73 deletions

View File

@ -9,11 +9,10 @@ import (
"github.com/gin-gonic/gin"
)
// Change the current connection in the session
func ChangeConnection(c *gin.Context) {
conn := c.PostForm("connected-database")
// Do something to change the connection
session := sessions.Default(c)
conn_bytes, ok := session.Get("connections").([]byte)
if !ok {
@ -39,5 +38,5 @@ func ChangeConnection(c *gin.Context) {
session.Set("current", name)
session.Save()
c.String(200, templates.ConnectionsList(connections, name))
c.String(200, templates.ConnectionsList(connections, name)+TableTree(c))
}

107
internal/database/tree.go Normal file
View File

@ -0,0 +1,107 @@
package database
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/Azpect3120/Web-Database-Viewer/internal/templates"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
func TableTree(c *gin.Context) string {
session := sessions.Default(c)
connections_bytes, ok := session.Get("connections").([]byte)
current, ok := session.Get("current").(string)
if !ok {
fmt.Println("No connections found")
return ""
}
var connections map[string]string
if err := json.Unmarshal(connections_bytes, &connections); err != nil {
fmt.Println(err)
return ""
}
url := connections[current]
tree, err := generateTree(url)
if err != nil {
fmt.Println(err)
return ""
}
fmt.Printf("%+v\n", tree)
return templates.TableTree(tree)
}
// Generate the tree of the database tables
func generateTree(url string) (map[string][]string, error) {
conn, err := sql.Open("postgres", url)
if err != nil {
return map[string][]string{}, err
}
defer conn.Close()
tree, err := tableList(conn)
if err != nil {
return map[string][]string{}, err
}
if err := fillColumns(conn, tree); err != nil {
return map[string][]string{}, err
}
return tree, nil
}
// Return a map with the keys being the table names and the values
// being blank which can be later used to store the columns.
func tableList(conn *sql.DB) (map[string][]string, error) {
rows, err := conn.Query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';")
if err != nil {
return map[string][]string{}, err
}
defer rows.Close()
tree := make(map[string][]string)
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return map[string][]string{}, err
}
tree[table] = []string{}
}
return tree, nil
}
// Fill the columns of the tables in the tree using the keys found
// in the tableList function.
//
// For now, the only data stored is the
// column name, but in the future this could be expanded to store
// datatype, constraints, primary keys, relationship, etc.
func fillColumns(conn *sql.DB, tree map[string][]string) error {
for table := range tree {
rows, err := conn.Query(fmt.Sprintf("SELECT column_name FROM information_schema.columns WHERE table_name = '%s';", table))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var column string
if err := rows.Scan(&column); err != nil {
return err
}
tree[table] = append(tree[table], column)
}
}
return nil
}

View File

@ -68,4 +68,8 @@ func populate(web, api *gin.RouterGroup) {
c.String(200, html)
})
api.POST("/connections/connect", database.ChangeConnection)
web.GET("/connections/tree", func(c *gin.Context) {
c.String(200, database.TableTree(c))
})
}

View File

@ -0,0 +1,76 @@
package templates
import (
"fmt"
"sort"
)
// Tree definition
const TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">`
const TREE_CLOSE string = `</ul>`
const TREE_BODY_TEMPLATE string = `<li>%s</li>`
// Table definition
const TABLE_TEMPLATE string = `
<button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
title="Select this table">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg>
%s
</button>
`
// Fields definition
const FIELDS_LIST_OPEN string = `<ul class="ml-6 mt-1 space-y-1 text-gray-600">`
const FIELDS_LIST_CLOSE string = `</ul>`
const FIELD_TEMPLATE string = `
<li>
<button class="flex items-center" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
<span>%s</span>
</button>
</li>
`
// This is not implemented yet
const PRIMARY_KEY string = `<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>`
// Generate the tree based on the database tables and columns
func TableTree(tree map[string][]string) string {
html := TREE_OPEN
var body string
for _, table := range getSortedKeys(tree) {
body += fmt.Sprintf(TABLE_TEMPLATE, table)
body += FIELDS_LIST_OPEN + generateFields(tree[table]) + FIELDS_LIST_CLOSE
}
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body)
return html + TREE_CLOSE
}
// Using a list of fields, generate the HTML for the fields
func generateFields(fields []string) string {
var html string
for _, field := range fields {
html += fmt.Sprintf(FIELD_TEMPLATE, field)
}
return html
}
// Return a list of the keys in a map, sorted alphabetically
func getSortedKeys(m map[string][]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

View File

@ -33,78 +33,22 @@
<div class="flex flex-col md:flex-row flex-grow">
<!-- Sidebar -->
<div class="w-full md:w-1/4 bg-white shadow-md">
<div class="p-4 border-b">
<div class="p-4 border-b flex justify-between items-center">
<h2 class="text-lg font-bold"><span id="database-name-tree">database</span> Tables</h2>
<button
hx-get="/v1/web/connections/tree"
hx-trigger="click"
hx-target="#database-table-tree"
class="hover:bg-gray-100 p-2 rounded-md"
>
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512">
<path d="M400 148l-21.12-24.57A191.43 191.43 0 00240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 00181.07-128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/>
<path d="M464 97.42V208a16 16 0 01-16 16H337.42c-14.26 0-21.4-17.23-11.32-27.31L436.69 86.1C446.77 76 464 83.16 464 97.42z"/>
</svg>
</button>
</div>
<div class="p-4 max-h-full">
<ul class="space-y-2">
<li>
<button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
title="Select this table">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg>
Table 1
</button>
<ul class="ml-6 mt-1 space-y-1 text-gray-600">
<li>
<button class="flex items-center" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
<span>Column 1</span>
<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>
</button>
</li>
<li>
<button class="flex items-center" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
<span>Column 2</span>
</button>
</li>
</ul>
</li>
<li>
<button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
title="Select this table">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg>
Table 2
</button>
<ul class="ml-6 mt-1 space-y-1 text-gray-600">
<li>
<button class="flex items-center" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
<span>Column A</span>
<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>
</button>
</li>
<li>
<button class="flex items-center" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
</path>
</svg>
<span>Column B</span>
</button>
</li>
</ul>
</li>
</ul>
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree" hx-trigger="load" hx-target="#database-table-tree">
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
</div>
</div>