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.Set("current", name)
session.Save() 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] url := connections[current]
tree, err := generateTree(url) tree, err := generateTableTree(url)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return "" return ""
@ -39,7 +39,7 @@ func TableTree(c *gin.Context) string {
} }
// Generate the tree of the database tables // 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) conn, err := sql.Open("postgres", url)
if err != nil { if err != nil {
return map[string][]model.Column{}, err 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) 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 { if err != nil {
return err return err
} }
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var column model.Column var (
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength); err != nil { column model.Column
enumType string
)
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength, &enumType); err != nil {
return err return err
} }
if column.Type == "USER-DEFINED" {
column.Type = enumType
}
if column.Name == pkey { if column.Name == pkey {
column.PrimaryKey = true column.PrimaryKey = true
} }
@ -172,3 +178,69 @@ func getUniqueColumns(conn *sql.DB, table string) ([]string, error) {
return cols, nil 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) 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)) 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) web.GET("/query/auto", templates.ToggleQueryType)

View File

@ -7,10 +7,10 @@ import (
"github.com/Azpect3120/Web-Database-Viewer/internal/model" "github.com/Azpect3120/Web-Database-Viewer/internal/model"
) )
// Tree definition // Table tree definition
const TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">` const TABLE_TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">`
const TREE_CLOSE string = `</ul>` const TABLE_TREE_CLOSE string = `</ul>`
const TREE_BODY_TEMPLATE string = `<li>%s</li>` const TABLE_TREE_BODY_TEMPLATE string = `<li>%s</li>`
// Table definition // Table definition
const TABLE_TEMPLATE string = ` const TABLE_TEMPLATE string = `
@ -26,9 +26,9 @@ const TABLE_TEMPLATE string = `
` `
// Fields definition // Fields definition
const FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">` const TABLE_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 TABLE_FIELDS_LIST_CLOSE string = `</ul>`
const FIELD_TEMPLATE string = ` const TABLE_FIELD_TEMPLATE string = `
<li> <li>
<button onclick="LoadTableQueryWithFields('%s', '%s')" class="flex items-center w-full" title="Select this field"> <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" <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> </li>
` `
// This is not implemented yet // Enum tree definition
const PRIMARY_KEY string = `<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>` 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 // Generate the tree based on the database tables and columns
func TableTree(tree map[string][]model.Column) string { func TableTree(tree map[string][]model.Column) string {
html := TREE_OPEN html := TABLE_TREE_OPEN
var body string var body string
for _, table := range getSortedKeys(tree) { for _, table := range getSortedKeys(tree) {
body += fmt.Sprintf(TABLE_TEMPLATE, table, table, table, table, table) body += fmt.Sprintf(TABLE_TEMPLATE, table, table, table, table, table)
fields := fmt.Sprintf(FIELDS_LIST_OPEN, table) fields := fmt.Sprintf(TABLE_FIELDS_LIST_OPEN, table)
body += fields + generateFields(table, tree[table]) + FIELDS_LIST_CLOSE body += fields + generateFields(table, tree[table]) + TABLE_FIELDS_LIST_CLOSE
} }
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body) html += fmt.Sprintf(TABLE_TREE_BODY_TEMPLATE, body)
return html + TREE_CLOSE return html + TABLE_TREE_CLOSE
} }
// Using a list of fields, generate the HTML for the fields // Using a list of fields, generate the HTML for the fields
func generateFields(table string, fields []model.Column) string { func generateFields(table string, fields []model.Column) string {
var html string var html string
for _, field := range fields { 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 html
} }
// Return a list of the keys in a map, sorted alphabetically // 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)) keys := make([]string, 0, len(m))
for k := range m { for k := range m {
keys = append(keys, k) keys = append(keys, k)
@ -106,3 +134,28 @@ func generateType(col model.Column) string {
return str 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 * This file also contains the functions that are used to generate quick queries for the
* tables. * 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) { function ToggleFields(id) {
const fields = document.getElementById(`fields-${id}`); const fields = document.getElementById(`fields-${id}`);
@ -29,3 +32,15 @@ function LoadTableQueryWithFields(table, fields) {
sql.dispatchEvent(new Event("input", { bubbles: true })); 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"> <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> <script src="https://unpkg.com/htmx.org@2.0.1"></script>
<link rel="icon" type="image/png" href="/v1/web/assets/favicon.ico"> <link rel="icon" type="image/png" href="/v1/web/assets/favicon.ico">
<style>
.htmx-request {
display: block;
}
</style>
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100">
@ -45,29 +40,37 @@
<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 flex justify-between items-center"> <div class="p-4 border-b flex justify-between items-center">
<h2 class="text-lg font-bold"> <h2 class="text-lg font-bold">
<span id="database-name-tree">database</span> <span id="database-name-tree">database</span>
<span id="table-loading" class="text-sm font-normal mx-1 htmx-indicator duration-300">loading </h2>
tables...</span> <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">
</h2> <svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512">
<button hx-get="/v1/web/connections/tree" hx-trigger="click" hx-target="#database-table-tree" <path
class="hover:bg-gray-100 p-2 rounded-md" hx-indicator="#table-loading"> 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"
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512"> fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" />
<path <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" 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" />
fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" /> </svg>
<path </button>
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" /> </div>
</svg> <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">
</button> <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> </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">
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
</div>
</div>
<!-- Main Content --> <!-- Main Content -->
<div class="w-full md:w-3/4 p-4"> <div class="w-full md:w-3/4 p-4">
@ -131,21 +134,27 @@
<form id="connection-form" class="grid grid-cols-2 gap-4"> <form id="connection-form" class="grid grid-cols-2 gap-4">
<div> <div>
<label for="db-host" class="block text-sm font-medium text-gray-700">Host</label> <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>
<div> <div>
<label for="db-port" class="block text-sm font-medium text-gray-700">Port</label> <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>
<div> <div>
<label for="db-username" class="block text-sm font-medium text-gray-700">Username</label> <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>
<div> <div>
<label for="db-password" class="block text-sm font-medium text-gray-700">Password</label> <label for="db-password" class="block text-sm font-medium text-gray-700">Password</label>
<div class="relative mt-1"> <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"> <input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●"
<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"> 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" <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"> 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> <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>
<div> <div>
<label for="db-database" class="block text-sm font-medium text-gray-700">Database Name</label> <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>
<div class="col-span-2"> <div class="col-span-2">
<label for="db-url" class="block text-sm font-medium text-gray-700"> <label for="db-url" class="block text-sm font-medium text-gray-700">
@ -180,7 +190,8 @@
will match the database name. will match the database name.
</span> </span>
</label> </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>
<div class="col-span-2"> <div class="col-span-2">
<label for="db-url" class="block text-sm font-medium text-gray-700"> <label for="db-url" class="block text-sm font-medium text-gray-700">
@ -201,15 +212,9 @@
</div> </div>
</form> </form>
<div class="flex items-center space-x-4 mt-4"> <div class="flex items-center space-x-4 mt-4">
<button <button hx-post="/v1/api/connections" hx-trigger="click" hx-target="#connected-database" hx-swap="outerHTML"
hx-post="/v1/api/connections" hx-include="#connection-form" hx-on::after-request="HideModal();"
hx-trigger="click" class="bg-blue-500 text-white px-4 py-2 rounded-md">
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 Create Connection
</button> </button>
<button hx-post="/v1/api/connections/test" hx-trigger="click" hx-swap="outerHTML" <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/modal.js"></script>
<script src="/v1/web/static/scripts/tree.js"></script> <script src="/v1/web/static/scripts/tree.js"></script>
</body> </body>
</html> </html>