FEAT: Sessions are complete!

Queries are also complete...
Needs some bug testing and such but the queries are running and
displaying!
This commit is contained in:
Azpect3120 2024-08-07 15:04:02 -07:00
parent 111a6aead3
commit 5cbe004318
13 changed files with 560 additions and 266 deletions

2
go.mod
View File

@ -22,7 +22,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect

2
go.sum
View File

@ -41,6 +41,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=

View File

@ -0,0 +1,43 @@
package database
import (
"encoding/json"
"fmt"
"github.com/Azpect3120/Web-Database-Viewer/internal/templates"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
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 {
c.String(200, templates.ConnectionsList(nil, ""))
return
}
var connections map[string]string
if err := json.Unmarshal(conn_bytes, &connections); err != nil {
c.String(200, templates.ConnectionsList(nil, ""))
fmt.Println(err)
return
}
var name string
for n, c := range connections {
if c == conn {
name = n
break
}
}
session.Set("current", name)
session.Save()
c.String(200, templates.ConnectionsList(connections, name))
}

View File

@ -1,31 +1,46 @@
package database package database
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/Azpect3120/Web-Database-Viewer/internal/templates"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Create a new connection to a database and store the details
// in the session.
func CreateConnection(c *gin.Context) { func CreateConnection(c *gin.Context) {
session := sessions.Default(c)
var ( var (
url string = c.PostForm("db-url") url string = c.PostForm("db-url")
database string = c.PostForm("db-database") database string = c.PostForm("db-database")
) )
connections, ok := session.Get("connections").(map[string]string) session := sessions.Default(c)
var connections map[string]string
session_bytes, ok := session.Get("connections").([]byte)
if !ok { if !ok {
fmt.Println("Creating new connections map /internal/database/create.go:19")
connections = make(map[string]string) connections = make(map[string]string)
} else {
if err := json.Unmarshal(session_bytes, &connections); err != nil {
fmt.Println(err)
}
} }
connections[database] = url connections[database] = url
session.Set("connections", connections) conn_bytes, err := json.Marshal(connections)
err := session.Save()
if err != nil { if err != nil {
fmt.Println("Failed to save session /internal/database/create.go:29")
fmt.Println(err) fmt.Println(err)
} }
session.Set("connections", []byte(conn_bytes))
session.Set("current", database)
session.Save()
html := templates.ConnectionsList(connections, database)
c.String(200, html)
} }

112
internal/database/query.go Normal file
View File

@ -0,0 +1,112 @@
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 QueryCurrent(c *gin.Context) {
query := c.PostForm("sql")
conn := getConnection(c)
cols, data, err := queryConnection(query, conn)
if err != nil {
c.String(200, templates.ErrorQueryResults(err))
return
}
c.String(200, templates.QueryResults(cols, data))
}
func queryConnection(query, url string) ([]string, []map[string]interface{}, error) {
db, err := sql.Open("postgres", url)
if err != nil {
return []string{}, []map[string]interface{}{}, err
}
rows, err := db.Query(query)
if err != nil {
return []string{}, []map[string]interface{}{}, err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return []string{}, []map[string]interface{}{}, err
}
// Create values pointer and value pointer slices
// We need to use pointers because the Scan function
// requires a pointer to the value. However, there
// is no simple way to create an array of pointers.
values := make([]interface{}, len(cols))
valuePtrs := make([]interface{}, len(cols))
for i := range cols {
valuePtrs[i] = &values[i]
}
// Final data structure to store the results
// An array of maps, where each map is a row
// and the keys are the column names.
var result []map[string]interface{}
for rows.Next() {
// Scan the result into the value pointers
err := rows.Scan(valuePtrs...)
if err != nil {
fmt.Println(err)
}
// Create a map to store the column data
row := make(map[string]interface{})
for i, col := range cols {
var v interface{}
val := values[i]
// Convert the value to a string representation
// I can't see when this would fail and return
// something that isn't a string, but it's better
// to be safe than sorry. However, this might be
// hard to handle in the frontend.
b, ok := val.([]byte)
if ok {
v = string(b)
} else {
v = val
}
row[col] = v
}
// Append the row to the result set
result = append(result, row)
}
return cols, result, nil
}
func getConnection(c *gin.Context) (url string) {
session := sessions.Default(c)
conn_bytes, ok := session.Get("connections").([]byte)
curr, ok := session.Get("current").(string)
if !ok {
fmt.Println("No current connection")
return ""
}
var connections map[string]string
if err := json.Unmarshal(conn_bytes, &connections); err != nil {
fmt.Println(err)
return ""
}
return connections[curr]
}

View File

@ -1,9 +1,12 @@
package http package http
import ( import (
"encoding/json"
"fmt"
"time" "time"
"github.com/Azpect3120/Web-Database-Viewer/internal/database" "github.com/Azpect3120/Web-Database-Viewer/internal/database"
"github.com/Azpect3120/Web-Database-Viewer/internal/templates"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -21,18 +24,48 @@ func populate(web, api *gin.RouterGroup) {
}) })
}) })
api.POST("/query", func(c *gin.Context) { api.POST("/query", database.QueryCurrent)
sql := c.PostForm("sql")
c.JSON(200, gin.H{"sql": sql})
})
api.POST("/connections/test", database.TestConnectionURL) api.POST("/connections/test", database.TestConnectionURL)
api.POST("/connections", database.CreateConnection) api.POST("/connections", database.CreateConnection)
api.GET("/connections", func(c *gin.Context) { api.GET("/connections", func(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
connections, ok := session.Get("connections").(map[string]string) connections_bytes, ok := session.Get("connections").([]byte)
if !ok {
c.JSON(200, gin.H{"connections": make(map[string]string), "current": "", "count": 0, "time": time.Now().String(), "status": 200})
return
}
current := session.Get("current")
c.JSON(200, gin.H{"okay": ok, "connections": connections}) var connections map[string]string
if err := json.Unmarshal(connections_bytes, &connections); err != nil {
fmt.Println(err)
}
c.JSON(200, gin.H{
"connections": connections,
"current": current,
"count": len(connections),
"time": time.Now().String(),
"status": 200,
})
}) })
web.GET("/connections", func(c *gin.Context) {
session := sessions.Default(c)
connections_bytes, conn_ok := session.Get("connections").([]byte)
current, curr_ok := session.Get("current").(string)
var connections map[string]string
if conn_ok || curr_ok {
if err := json.Unmarshal(connections_bytes, &connections); err != nil {
fmt.Println(err)
}
} else {
connections = make(map[string]string)
}
html := templates.ConnectionsList(connections, current)
c.String(200, html)
})
api.POST("/connections/connect", database.ChangeConnection)
} }

View File

@ -1,9 +1,13 @@
package http package http
import ( import (
"encoding/gob"
"fmt" "fmt"
"net/http"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -22,18 +26,28 @@ func New(port string) *Server {
Router: gin.Default(), Router: gin.Default(),
config: cors.DefaultConfig(), config: cors.DefaultConfig(),
} }
server.config.AllowOrigins = []string{"*"} server.config.AllowOrigins = []string{"*"}
server.Router.Use(cors.New(server.config)) server.Router.Use(cors.New(server.config))
// Session configuration
gob.Register([]byte{})
gob.Register(map[string]string{})
store := cookie.NewStore([]byte("secret"))
store.Options(sessions.Options{
Path: "/",
Domain: "",
MaxAge: 86400 * 7, // 7 days
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
server.Router.Use(sessions.Sessions("mysession", store))
return server return server
} }
// Setup the server with the necessary configurations // Setup the server with the necessary configurations
func (s *Server) Setup() *Server { func (s *Server) Setup() *Server {
// This has to be first ALWAYS for some stupid reason :|
s.initSession()
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
web_g := v1.Group("/web") web_g := v1.Group("/web")
api_g := v1.Group("/api") api_g := v1.Group("/api")

View File

@ -1,18 +0,0 @@
package http
import (
"encoding/gob"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
)
const SESSION_SECRET string = "lqMVy04h8KWll6lCU9XCDbfUsBIjD1eG"
// Initialize the session
// Session is a map of "db name": "connection url"
func (s *Server) initSession() {
gob.Register(&map[string]string{})
store := cookie.NewStore([]byte(SESSION_SECRET))
s.Router.Use(sessions.Sessions("mysession", store))
}

View File

@ -0,0 +1,29 @@
package templates
import "fmt"
const LIST_OPEN string = `<select id="connected-database" name="connected-database" class="mt-1 block p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm md:text-base">`
const LIST_ITEM string = `<option value="%s"%s>%s</option>`
const LIST_CLOSE string = `</select>`
// Generate a list of connections to display in the drop-down.
// Current connection will be toggled as selected
func ConnectionsList(connections map[string]string, current string) string {
var html string = LIST_OPEN
if len(connections) == 0 || connections == nil {
html += fmt.Sprintf(LIST_ITEM, "", " selected", "No connections")
return html + LIST_CLOSE
}
for name, url := range connections {
if name == current {
html += fmt.Sprintf(LIST_ITEM, url, " selected", name)
} else {
html += fmt.Sprintf(LIST_ITEM, url, "", name)
}
}
html += LIST_CLOSE
return html
}

View File

@ -0,0 +1,63 @@
package templates
import "fmt"
// Table wrapper definitions
const table_open string = `<table id="query-result" class="min-w-full divide-y divide-gray-200">`
const table_close string = `</table>`
// Header definitions
const table_head_open string = `<thead class="bg-gray-50"><tr>`
const table_head_close string = `</tr></thead>`
const table_head_row string = `<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">%s</th>`
// Body definitions
const table_body_open string = `<tbody class="bg-white divide-y divide-gray-200">`
const table_body_close string = `</tbody>`
const table_body_row string = `<td class="px-6 py-4 whitespace-nowrap">%v</td>`
// Error message
const query_error_message string = `<p id="query-error" hx-swap-oob="outerHTML" class="text-red-500 py-2 text-sm">Query Error: %s</p>`
const query_error_message_blank string = `<p id="query-error" hx-swap-oob="outerHTML" class="text-red-500 py-2 text-sm hidden"></p>`
func ErrorQueryResults(e error) string {
return table_open + table_close + fmt.Sprintf(query_error_message, e.Error())
}
func QueryResults(cols []string, rows []map[string]interface{}) string {
head := generateHead(cols)
body := table_body_open
for _, row := range rows {
body += generateRow(cols, row)
}
body += table_body_close
return table_open + head + body + table_close + query_error_message_blank
}
// Generate the tables head row
func generateHead(cols []string) string {
html := table_head_open
for _, col := range cols {
html += fmt.Sprintf(table_head_row, col)
}
return html + table_head_close
}
func generateRow(cols []string, data map[string]interface{}) string {
row := "<tr>"
for _, col := range cols {
str, ok := data[col].(string)
if !ok {
str = "NULL"
}
row += fmt.Sprintf(table_body_row, str)
}
return row + "</tr>"
}

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
./tools/tailwind -i ./public/assets/styles/config.css -o ./public/assets/styles/main.css --minify ./tools/tailwind -i ./web/static/styles/config.css -o ./web/static/styles/main.css --minify

View File

@ -829,6 +829,11 @@ video {
background-color: rgb(229 231 235 / var(--tw-bg-opacity)); background-color: rgb(229 231 235 / var(--tw-bg-opacity));
} }
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
}
.bg-gray-50 { .bg-gray-50 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity)); background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@ -839,11 +844,6 @@ video {
background-color: rgb(75 85 99 / var(--tw-bg-opacity)); background-color: rgb(75 85 99 / var(--tw-bg-opacity));
} }
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -900,6 +900,11 @@ video {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.pb-2 { .pb-2 {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
@ -937,6 +942,10 @@ video {
font-weight: 700; font-weight: 700;
} }
.font-light {
font-weight: 300;
}
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
@ -964,9 +973,9 @@ video {
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
} }
.text-green-500 { .text-red-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity)); color: rgb(239 68 68 / var(--tw-text-opacity));
} }
.text-white { .text-white {

View File

@ -1,244 +1,236 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Database Query Tool</title>
<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">
</head>
<body class="bg-gray-100">
<div class="flex flex-col h-screen"> <head>
<!-- Top Bar --> <meta charset="UTF-8">
<div class="bg-white shadow-md p-4 flex items-center justify-between border-b"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<div> <title>Database Query Tool</title>
<h1 class="text-2xl font-bold">Database Query Tool</h1> <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<p class="text-sm text-gray-600">Connect and query your databases effortlessly.</p> <script src="https://unpkg.com/htmx.org@2.0.1"></script>
</div> <link rel="icon" type="image/png" href="/v1/web/assets/favicon.ico">
</head>
<body class="bg-gray-100">
<div class="flex flex-col h-screen">
<!-- Top Bar -->
<div class="bg-white shadow-md p-4 flex items-center justify-between border-b">
<div>
<h1 class="text-2xl font-bold">Database Query Tool</h1>
<p class="text-sm text-gray-600">Connect and query your databases effortlessly.</p>
</div>
<div class="flex items-center justify-end space-x-4 flex-wrap"> <div class="flex items-center justify-end space-x-4 flex-wrap">
<label for="database" class="block text-sm font-medium text-gray-700">Connected Database:</label> <form hx-post="/v1/api/connections/connect" hx-trigger="change" hx-swap="outerHTML" hx-target="#connected-database" hx-encoding="multipart/form-data" class="flex items-center justify-end space-x-2 flex-wrap">
<select id="database" class="mt-1 block p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm md:text-base"> <label for="connected-database" class="block text-sm font-medium text-gray-700">Connected Database:</label>
<option value="database1">Database 1</option> <select hx-get="/v1/web/connections" hx-trigger="load" hx-swap="outerHTML" id="connected-database" name="connected-database" class="mt-1 block p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm md:text-base"></select>
<option value="database2">Database 2</option> </form>
</select>
<button onclick="ShowModal();" class="bg-blue-500 text-white px-4 py-2 my-2 rounded-md text-sm md:text-base"> <button onclick="ShowModal();" class="bg-blue-500 text-white px-4 py-2 my-2 rounded-md text-sm md:text-base">
Add Connection Add Connection
</button> </button>
</div> </div>
</div> </div>
<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">
<h2 class="text-lg font-bold"><em>Database 1</em> Tables</h2> <h2 class="text-lg font-bold"><em>Database 1</em> Tables</h2>
</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>
</div> </div>
<div class="p-4 max-h-full">
<!-- Main Content --> <ul class="space-y-2">
<div class="w-full md:w-3/4 p-4"> <li>
<main> <button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
<!-- Query Input --> title="Select this table">
<form <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
class="mb-4" xmlns="http://www.w3.org/2000/svg">
hx-post="/v1/api/query" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
hx-trigger="input delay:1000ms" </svg>
hx-swap="none" Table 1
hx-target="" </button>
> <ul class="ml-6 mt-1 space-y-1 text-gray-600">
<label for="sql" class="block text-sm font-medium text-gray-700">SQL Query</label> <li>
<textarea <button class="flex items-center" title="Select this field">
id="sql" <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
name="sql" xmlns="http://www.w3.org/2000/svg">
rows="4" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
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" </path>
></textarea>
</form>
<!-- Query Results -->
<div class="overflow-x-auto overflow-y-hidden bg-white shadow-md rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Column 1</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Column 2</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap">Data 1</td>
<td class="px-6 py-4 whitespace-nowrap">Data 2</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap">Data A</td>
<td class="px-6 py-4 whitespace-nowrap">Data B</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
</div>
</div>
<!-- Create Connection Modal -->
<div id="connection-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden opacity-0">
<div class="flex items-center justify-center min-h-screen">
<div class="bg-white p-6 rounded-lg shadow-lg w-2/3">
<div class="flex justify-between items-center border-b pb-2">
<h2 class="text-xl font-bold">Add New Connection</h2>
<button onclick="HideModal();" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" 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 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="mt-4">
<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" value="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" value="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" value="azpect" 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="●●●●●●●●●"
value="Panther4487!!!!"
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>
<circle cx="12" cy="12" r="3"></circle>
</svg> </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> </button>
</div> </li>
</div> <li>
<div> <button class="flex items-center" title="Select this field">
<label for="db-driver" class="block text-sm font-medium text-gray-700">Driver/Type of Database</label> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<select id="db-driver" name="db-driver" 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"> xmlns="http://www.w3.org/2000/svg">
<option value="postgresql">PostgreSQL</option> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
<option value="mysql">MySQL</option> </path>
<option value="sqlite">SQLite</option> </svg>
<option value="sqlserver">SQL Server</option> <span>Column 2</span>
<option value="oracle">Oracle</option> </button>
<option value="mariadb">MariaDB</option> </li>
<option value="db2">DB2</option> </ul>
</select> </li>
</div> <li>
<div> <button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
<label for="db-database" class="block text-sm font-medium text-gray-700">Database Name</label> title="Select this table">
<input id="db-database" name="db-database" type="text" placeholder="master_database" value="azpect" 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"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
</div> xmlns="http://www.w3.org/2000/svg">
<div class="col-span-2"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
<label for="db-url" class="block text-sm font-medium text-gray-700"> </svg>
Connection URL Table 2
<br>
<span class="text-xs font-light">
The connection URL will be automatically generated based on the above fields. To view the URL generated,
push the "display secret details" button in the password section.
</span>
</label>
<input id="db-url" name="db-url" type="password" placeholder="postgresql://admin:password@127.0.0.1:5432/master_database" value="postgresql://azpect:Panther4487!!!!@127.0.0.1:5432/azpect?sslmode=disable" 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">
<span id="db-url-invalid" class="text-xs text-red-500 hidden">Connection URL is incomplete or invalid.</span>
</div>
</form>
<div class="flex items-center space-x-4 mt-4">
<button
hx-post="/v1/api/connections"
hx-trigger="click"
hx-swap="none"
hx-include="#connection-form"
onclick="HideModal();"
class="bg-blue-500 text-white px-4 py-2 rounded-md"
>
Create Connection
</button> </button>
<button hx-post="/v1/api/connections/test" hx-trigger="click" hx-swap="outerHTML" hx-target="#connection-status" hx-include="#connection-form" hx-encoding="multipart/form-data" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-md flex items-center space-x-2"> <ul class="ml-6 mt-1 space-y-1 text-gray-600">
<span>Test Connection</span> <li>
<span id="connection-status" class="w-3 h-3 rounded-full bg-gray-400"></span> <button class="flex items-center" title="Select this field">
</button> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<span id="connection-message"></span> 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>
<!-- Main Content -->
<div class="w-full md:w-3/4 p-4">
<main>
<!-- Query Input -->
<form class="mb-4" hx-post="/v1/api/query" hx-trigger="input delay:1000ms" hx-swap="outerHTML" hx-target="#query-result">
<label for="sql" class="block text-sm font-medium text-gray-700">SQL Query</label>
<textarea id="sql" name="sql" rows="4" 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"></textarea>
<p id="query-error" hx-swap-oob="outerHTML" class="text-red-500 py-2 text-sm hidden"></p>
</form>
<!-- Query Results -->
<div class="overflow-x-auto overflow-y-hidden bg-white shadow-md rounded-lg">
<table id="query-result" class="min-w-full divide-y divide-gray-200"></table>
</div>
</main>
</div>
</div>
</div>
<!-- Create Connection Modal -->
<div id="connection-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden opacity-0">
<div class="flex items-center justify-center min-h-screen">
<div class="bg-white p-6 rounded-lg shadow-lg w-2/3">
<div class="flex justify-between items-center border-b pb-2">
<h2 class="text-xl font-bold">Add New Connection</h2>
<button onclick="HideModal();" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" 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 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="mt-4">
<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" value="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>
<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" value="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" value="azpect"
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="●●●●●●●●●"
value="Panther4487!!!!"
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>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
<div>
<label for="db-driver" class="block text-sm font-medium text-gray-700">Driver/Type of Database</label>
<select id="db-driver" name="db-driver"
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">
<option value="postgresql">PostgreSQL</option>
<option value="mysql">MySQL</option>
<option value="sqlite">SQLite</option>
<option value="sqlserver">SQL Server</option>
<option value="oracle">Oracle</option>
<option value="mariadb">MariaDB</option>
<option value="db2">DB2</option>
</select>
</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" value="azpect"
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">
Connection URL
<br>
<span class="text-xs font-light">
The connection URL will be automatically generated based on the above fields. To view the URL
generated,
push the "display secret details" button in the password section.
</span>
</label>
<input id="db-url" name="db-url" type="password"
placeholder="postgresql://admin:password@127.0.0.1:5432/master_database"
value="postgresql://azpect:Panther4487!!!!@127.0.0.1:5432/azpect?sslmode=disable"
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">
<span id="db-url-invalid" class="text-xs text-red-500 hidden">Connection URL is incomplete or
invalid.</span>
</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" onclick="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"
hx-target="#connection-status" hx-include="#connection-form" hx-encoding="multipart/form-data"
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-md flex items-center space-x-2">
<span>Test Connection</span>
<span id="connection-status" class="w-3 h-3 rounded-full bg-gray-400"></span>
</button>
<span id="connection-message"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Scripts -->
<script src="/v1/web/static/scripts/password.js"></script>
<script src="/v1/web/static/scripts/modal.js"></script>
</body>
<!-- Scripts -->
<script src="/v1/web/static/scripts/password.js"></script>
<script src="/v1/web/static/scripts/modal.js"></script>
</body>
</html> </html>