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"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Change the current connection in the session
|
||||||
func ChangeConnection(c *gin.Context) {
|
func ChangeConnection(c *gin.Context) {
|
||||||
conn := c.PostForm("connected-database")
|
conn := c.PostForm("connected-database")
|
||||||
|
|
||||||
// Do something to change the connection
|
|
||||||
|
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
conn_bytes, ok := session.Get("connections").([]byte)
|
conn_bytes, ok := session.Get("connections").([]byte)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -39,5 +38,5 @@ func ChangeConnection(c *gin.Context) {
|
|||||||
session.Set("current", name)
|
session.Set("current", name)
|
||||||
session.Save()
|
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)
|
c.String(200, html)
|
||||||
})
|
})
|
||||||
api.POST("/connections/connect", database.ChangeConnection)
|
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">
|
<div class="flex flex-col md:flex-row flex-grow">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="w-full md:w-1/4 bg-white shadow-md">
|
<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>
|
<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>
|
||||||
<div class="p-4 max-h-full">
|
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree" hx-trigger="load" hx-target="#database-table-tree">
|
||||||
<ul class="space-y-2">
|
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user