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:
parent
fb548e4207
commit
276a6be7b9
@ -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
107
internal/database/tree.go
Normal 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
|
||||
}
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
76
internal/templates/tree.go
Normal file
76
internal/templates/tree.go
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user