FEAT: Enum support all over!

Enum table is generated below the Tables tree. Enums cannot be queried
since I don't know what I would want them to query so right now its just
for viewing. However, enum types are now displayed properly in the table
tree. I still don't know how strong the support will be for other kinds
of DBs that aren't PSQL.
This commit is contained in:
Azpect3120 2024-08-15 13:28:43 -07:00
parent 8ab00b20d4
commit 83dafb8a08
7 changed files with 1409 additions and 67 deletions

View File

@ -38,5 +38,5 @@ func ChangeConnection(c *gin.Context) {
session.Set("current", name)
session.Save()
c.String(200, templates.ConnectionsList(connections, name)+TableTree(c))
c.String(200, templates.ConnectionsList(connections, name)+TableTree(c)+EnumTree(c))
}

View File

@ -29,7 +29,7 @@ func TableTree(c *gin.Context) string {
url := connections[current]
tree, err := generateTree(url)
tree, err := generateTableTree(url)
if err != nil {
fmt.Println(err)
return ""
@ -39,7 +39,7 @@ func TableTree(c *gin.Context) string {
}
// Generate the tree of the database tables
func generateTree(url string) (map[string][]model.Column, error) {
func generateTableTree(url string) (map[string][]model.Column, error) {
conn, err := sql.Open("postgres", url)
if err != nil {
return map[string][]model.Column{}, err
@ -118,17 +118,23 @@ func fillColumns(conn *sql.DB, tree map[string][]model.Column) error {
fkeys = append(fkeys, fkey)
}
rows, err := conn.Query(fmt.Sprintf("SELECT column_name, is_nullable, data_type, character_maximum_length FROM information_schema.columns WHERE table_name = '%s';", table))
rows, err := conn.Query(fmt.Sprintf("SELECT c.column_name, c.is_nullable, c.data_type, c.character_maximum_length, t.typname AS enum_type FROM information_schema.columns c JOIN pg_type t ON c.udt_name = t.typname WHERE c.table_name = '%s';", table))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var column model.Column
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength); err != nil {
var (
column model.Column
enumType string
)
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength, &enumType); err != nil {
return err
}
if column.Type == "USER-DEFINED" {
column.Type = enumType
}
if column.Name == pkey {
column.PrimaryKey = true
}
@ -172,3 +178,69 @@ func getUniqueColumns(conn *sql.DB, table string) ([]string, error) {
return cols, nil
}
// Generate the tree of the database enums and their values
func EnumTree(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]
enums, err := genereteEnumTree(url)
if err != nil {
fmt.Println(err)
return ""
}
return templates.EnumTree(enums)
}
// Generate the tree of the database enums and their values from a
// provided connection URL.
func genereteEnumTree(url string) (map[string][]string, error) {
conn, err := sql.Open("postgres", url)
if err != nil {
return nil, err
}
defer conn.Close()
enums, err := enumList(conn)
if err != nil {
return nil, err
}
return enums, nil
}
// Get a list/map of all the enums in the database.
// The key is the name of the enum and the value is a slice of the enum values.
func enumList(conn *sql.DB) (map[string][]string, error) {
rows, err := conn.Query("SELECT t.typname AS enum_name, e.enumlabel AS enum_value FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typcategory = 'E' AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY t.typname, e.enumsortorder;")
if err != nil {
return map[string][]string{}, err
}
defer rows.Close()
enums := make(map[string][]string)
for rows.Next() {
var enum, value string
if err := rows.Scan(&enum, &value); err != nil {
return map[string][]string{}, err
}
enums[enum] = append(enums[enum], value)
}
return enums, nil
}

View File

@ -70,9 +70,15 @@ func populate(web, api *gin.RouterGroup) {
})
api.POST("/connections/connect", database.ChangeConnection)
web.GET("/connections/tree", func(c *gin.Context) {
web.GET("/connections/tree/table", func(c *gin.Context) {
c.String(200, database.TableTree(c))
})
web.GET("/connections/tree/enum", func(c *gin.Context) {
c.String(200, database.EnumTree(c))
})
web.GET("/connections/tree", func(c *gin.Context) {
c.String(200, database.TableTree(c)+database.EnumTree(c))
})
web.GET("/query/auto", templates.ToggleQueryType)

View File

@ -7,10 +7,10 @@ import (
"github.com/Azpect3120/Web-Database-Viewer/internal/model"
)
// 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 tree definition
const TABLE_TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">`
const TABLE_TREE_CLOSE string = `</ul>`
const TABLE_TREE_BODY_TEMPLATE string = `<li>%s</li>`
// Table definition
const TABLE_TEMPLATE string = `
@ -26,9 +26,9 @@ const TABLE_TEMPLATE string = `
`
// Fields definition
const FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
const FIELDS_LIST_CLOSE string = `</ul>`
const FIELD_TEMPLATE string = `
const TABLE_FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
const TABLE_FIELDS_LIST_CLOSE string = `</ul>`
const TABLE_FIELD_TEMPLATE string = `
<li>
<button onclick="LoadTableQueryWithFields('%s', '%s')" class="flex items-center w-full" title="Select this field">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@ -42,35 +42,63 @@ const FIELD_TEMPLATE string = `
</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>`
// Enum tree definition
const ENUM_TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-enum-tree" class="space-y-2">`
const ENUM_TREE_CLOSE string = `</ul>`
const ENUM_TREE_BODY_TEMPLATE string = `<li>%s</li>`
// Enum definition
const ENUM_TEMPLATE string = `
<button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center">
<svg onclick="ToggleEnumValues('%s');" id="icon-enum-squeeze" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" transform="rotate(-90)">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg>
<span class="hover:underline py-1" onclick="ToggleEnumValues('%s');">%s</span>
</button>
`
// Enum values definition
const ENUM_VALUES_LIST_OPEN string = `<ul id="enum-values-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
const ENUM_VALUES_LIST_CLOSE string = `</ul>`
const ENUM_VALUE_TEMPLATE string = `
<li>
<div class="flex items-center w-full py-2">
<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>
</div>
</li>
`
// Generate the tree based on the database tables and columns
func TableTree(tree map[string][]model.Column) string {
html := TREE_OPEN
html := TABLE_TREE_OPEN
var body string
for _, table := range getSortedKeys(tree) {
body += fmt.Sprintf(TABLE_TEMPLATE, table, table, table, table, table)
fields := fmt.Sprintf(FIELDS_LIST_OPEN, table)
body += fields + generateFields(table, tree[table]) + FIELDS_LIST_CLOSE
fields := fmt.Sprintf(TABLE_FIELDS_LIST_OPEN, table)
body += fields + generateFields(table, tree[table]) + TABLE_FIELDS_LIST_CLOSE
}
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body)
return html + TREE_CLOSE
html += fmt.Sprintf(TABLE_TREE_BODY_TEMPLATE, body)
return html + TABLE_TREE_CLOSE
}
// Using a list of fields, generate the HTML for the fields
func generateFields(table string, fields []model.Column) string {
var html string
for _, field := range fields {
html += fmt.Sprintf(FIELD_TEMPLATE, table, field.Name, field.Name, generateType(field))
html += fmt.Sprintf(TABLE_FIELD_TEMPLATE, table, field.Name, field.Name, generateType(field))
}
return html
}
// Return a list of the keys in a map, sorted alphabetically
func getSortedKeys(m map[string][]model.Column) []string {
func getSortedKeys[T model.Column | string](m map[string][]T) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
@ -106,3 +134,28 @@ func generateType(col model.Column) string {
return str
}
// Generate the HTML string for the enum tree
func EnumTree(enums map[string][]string) string {
html := ENUM_TREE_OPEN
var body string
for _, enum := range getSortedKeys(enums) {
body += fmt.Sprintf(ENUM_TEMPLATE, enum, enum, enum)
valuesList := fmt.Sprintf(ENUM_VALUES_LIST_OPEN, enum)
body += valuesList + generateEnumValues(enums[enum]) + ENUM_VALUES_LIST_CLOSE
}
html += fmt.Sprintf(ENUM_TREE_BODY_TEMPLATE, body)
return html + ENUM_TREE_CLOSE
}
// Convert a list of values into a list of HTML elements
func generateEnumValues(values []string) string {
var html string
for _, value := range values {
html += fmt.Sprintf(ENUM_VALUE_TEMPLATE, value)
}
return html
}

View File

@ -4,6 +4,9 @@
*
* This file also contains the functions that are used to generate quick queries for the
* tables.
*
* This file also contains the functions that are used to toggle the visibility of the
* enum values in the tree view of the tables.
*/
function ToggleFields(id) {
const fields = document.getElementById(`fields-${id}`);
@ -29,3 +32,15 @@ function LoadTableQueryWithFields(table, fields) {
sql.dispatchEvent(new Event("input", { bubbles: true }));
}
function ToggleEnumValues(id) {
const enum_values = document.getElementById(`enum-values-${id}`);
const button_svg = document.getElementById(`icon-enum-${id}`);
if (enum_values.classList.contains("hidden")) {
enum_values.classList.remove("hidden");
button_svg.setAttribute("transform", "rotate(0)");
} else {
enum_values.classList.add("hidden");
button_svg.setAttribute("transform", "rotate(-90)");
}
}

File diff suppressed because one or more lines are too long

View File

@ -8,11 +8,6 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
<link rel="icon" type="image/png" href="/v1/web/assets/favicon.ico">
<style>
.htmx-request {
display: block;
}
</style>
</head>
<body class="bg-gray-100">
@ -49,11 +44,8 @@
<div class="p-4 border-b flex justify-between items-center">
<h2 class="text-lg font-bold">
<span id="database-name-tree">database</span>
<span id="table-loading" class="text-sm font-normal mx-1 htmx-indicator duration-300">loading
tables...</span>
</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" hx-indicator="#table-loading">
<button hx-get="/v1/web/connections/tree" hx-trigger="click" hx-swap="none" class="hover:bg-gray-100 p-2 rounded-md" hx-indicator="#table-loading">
<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"
@ -63,10 +55,21 @@
</svg>
</button>
</div>
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree" hx-trigger="load" hx-params="none"
hx-indicator="#table-loading" hx-target="#database-table-tree">
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree/table" hx-trigger="load" hx-params="none" hx-indicator="#table-loading" hx-target="#database-table-tree">
<div class="w-full flex items-center justify-between border-b pb-4 pt-2">
<h2 class="text-lg text-gray-700">Tables</h2>
<p id="table-loading" class="text-xs font-light htmx-indicator">Loading...</p>
</div>
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
</div>
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree/enum" hx-trigger="load" hx-params="none" hx-indicator="#enum-loading" hx-target="#database-enum-tree">
<div class="w-full flex items-center justify-between border-b pb-4 pt-2">
<h2 class="text-lg text-gray-700">Enums</h2>
<p id="enum-loading" class="text-xs font-light htmx-indicator">Loading...</p>
</div>
<ul hx-swap-oob="outerHTML" id="database-enum-tree" class="space-y-2"></ul>
</div>
</div>
<!-- Main Content -->
@ -131,21 +134,27 @@
<form id="connection-form" class="grid grid-cols-2 gap-4">
<div>
<label for="db-host" class="block text-sm font-medium text-gray-700">Host</label>
<input id="db-host" name="db-host" type="text" placeholder="127.0.0.1" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input id="db-host" name="db-host" type="text" placeholder="127.0.0.1"
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div>
<label for="db-port" class="block text-sm font-medium text-gray-700">Port</label>
<input id="db-port" name="db-port" type="text" placeholder="5432" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input id="db-port" name="db-port" type="text" placeholder="5432"
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div>
<label for="db-username" class="block text-sm font-medium text-gray-700">Username</label>
<input id="db-username" name="db-username" type="text" placeholder="admin" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input id="db-username" name="db-username" type="text" placeholder="admin"
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div>
<label for="db-password" class="block text-sm font-medium text-gray-700">Password</label>
<div class="relative mt-1">
<input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●" class="block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 px-3 py-2 text-gray-500 bg-gray-200 rounded-r-md border border-gray-300" title="Display secret details">
<input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●"
class="block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<button type="button" id="togglePassword"
class="absolute inset-y-0 right-0 px-3 py-2 text-gray-500 bg-gray-200 rounded-r-md border border-gray-300"
title="Display secret details">
<svg id="eyeIcon" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s3.5-7 11-7 11 7 11 7-3.5 7-11 7S1 12 1 12z"></path>
@ -169,7 +178,8 @@
</div>
<div>
<label for="db-database" class="block text-sm font-medium text-gray-700">Database Name</label>
<input id="db-database" name="db-database" type="text" placeholder="master_database" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input id="db-database" name="db-database" type="text" placeholder="master_database"
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div class="col-span-2">
<label for="db-url" class="block text-sm font-medium text-gray-700">
@ -180,7 +190,8 @@
will match the database name.
</span>
</label>
<input name="db-conn-name" id="db-conn-name" placeholder="master_database" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<input name="db-conn-name" id="db-conn-name" placeholder="master_database"
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<div class="col-span-2">
<label for="db-url" class="block text-sm font-medium text-gray-700">
@ -201,15 +212,9 @@
</div>
</form>
<div class="flex items-center space-x-4 mt-4">
<button
hx-post="/v1/api/connections"
hx-trigger="click"
hx-target="#connected-database"
hx-swap="outerHTML"
hx-include="#connection-form"
hx-on::after-request="HideModal();"
class="bg-blue-500 text-white px-4 py-2 rounded-md"
>
<button hx-post="/v1/api/connections" hx-trigger="click" hx-target="#connected-database" hx-swap="outerHTML"
hx-include="#connection-form" hx-on::after-request="HideModal();"
class="bg-blue-500 text-white px-4 py-2 rounded-md">
Create Connection
</button>
<button hx-post="/v1/api/connections/test" hx-trigger="click" hx-swap="outerHTML"
@ -234,4 +239,5 @@
<script src="/v1/web/static/scripts/modal.js"></script>
<script src="/v1/web/static/scripts/tree.js"></script>
</body>
</html>