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{"okay": ok, "connections": connections}) c.JSON(200, gin.H{"connections": make(map[string]string), "current": "", "count": 0, "time": time.Now().String(), "status": 200})
return
}) }
current := session.Get("current")
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,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -8,6 +9,7 @@
<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">
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100">
<div class="flex flex-col h-screen"> <div class="flex flex-col h-screen">
@ -18,11 +20,10 @@
<p class="text-sm text-gray-600">Connect and query your databases effortlessly.</p> <p class="text-sm text-gray-600">Connect and query your databases effortlessly.</p>
</div> </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>
@ -38,8 +39,10 @@
<div class="p-4 max-h-full"> <div class="p-4 max-h-full">
<ul class="space-y-2"> <ul class="space-y-2">
<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"> <button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg> </svg>
Table 1 Table 1
@ -47,8 +50,10 @@
<ul class="ml-6 mt-1 space-y-1 text-gray-600"> <ul class="ml-6 mt-1 space-y-1 text-gray-600">
<li> <li>
<button class="flex items-center" title="Select this field"> <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"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path> 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> </svg>
<span>Column 1</span> <span>Column 1</span>
<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span> <span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>
@ -56,8 +61,10 @@
</li> </li>
<li> <li>
<button class="flex items-center" title="Select this field"> <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"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path> 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> </svg>
<span>Column 2</span> <span>Column 2</span>
</button> </button>
@ -65,8 +72,10 @@
</ul> </ul>
</li> </li>
<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"> <button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center"
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
</svg> </svg>
Table 2 Table 2
@ -74,8 +83,10 @@
<ul class="ml-6 mt-1 space-y-1 text-gray-600"> <ul class="ml-6 mt-1 space-y-1 text-gray-600">
<li> <li>
<button class="flex items-center" title="Select this field"> <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"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path> 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> </svg>
<span>Column A</span> <span>Column A</span>
<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span> <span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>
@ -83,8 +94,10 @@
</li> </li>
<li> <li>
<button class="flex items-center" title="Select this field"> <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"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path> 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> </svg>
<span>Column B</span> <span>Column B</span>
</button> </button>
@ -99,42 +112,15 @@
<div class="w-full md:w-3/4 p-4"> <div class="w-full md:w-3/4 p-4">
<main> <main>
<!-- Query Input --> <!-- Query Input -->
<form <form class="mb-4" hx-post="/v1/api/query" hx-trigger="input delay:1000ms" hx-swap="outerHTML" hx-target="#query-result">
class="mb-4"
hx-post="/v1/api/query"
hx-trigger="input delay:1000ms"
hx-swap="none"
hx-target=""
>
<label for="sql" class="block text-sm font-medium text-gray-700">SQL Query</label> <label for="sql" class="block text-sm font-medium text-gray-700">SQL Query</label>
<textarea <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>
id="sql" <p id="query-error" hx-swap-oob="outerHTML" class="text-red-500 py-2 text-sm hidden"></p>
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>
</form> </form>
<!-- Query Results --> <!-- Query Results -->
<div class="overflow-x-auto overflow-y-hidden bg-white shadow-md rounded-lg"> <div class="overflow-x-auto overflow-y-hidden bg-white shadow-md rounded-lg">
<table class="min-w-full divide-y divide-gray-200"> <table id="query-result" class="min-w-full divide-y divide-gray-200"></table>
<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> </div>
</main> </main>
</div> </div>
@ -148,7 +134,8 @@
<div class="flex justify-between items-center border-b pb-2"> <div class="flex justify-between items-center border-b pb-2">
<h2 class="text-xl font-bold">Add New Connection</h2> <h2 class="text-xl font-bold">Add New Connection</h2>
<button onclick="HideModal();" class="text-gray-500 hover:text-gray-700"> <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"> <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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>
</button> </button>
@ -157,29 +144,30 @@
<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" 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"> <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> <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" 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"> <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>
<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" 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"> <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>
<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 <input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●"
id="db-password"
name="db-password"
type="password"
placeholder="●●●●●●●●●"
value="Panther4487!!!!" 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" 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"
<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="absolute inset-y-0 right-0 px-3 py-2 text-gray-500 bg-gray-200 rounded-r-md border border-gray-300"
<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"> 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> <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> <circle cx="12" cy="12" r="3"></circle>
</svg> </svg>
@ -188,7 +176,8 @@
</div> </div>
<div> <div>
<label for="db-driver" class="block text-sm font-medium text-gray-700">Driver/Type of Database</label> <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"> <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="postgresql">PostgreSQL</option>
<option value="mysql">MySQL</option> <option value="mysql">MySQL</option>
<option value="sqlite">SQLite</option> <option value="sqlite">SQLite</option>
@ -200,33 +189,35 @@
</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" 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"> <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>
<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">
Connection URL Connection URL
<br> <br>
<span class="text-xs font-light"> <span class="text-xs font-light">
The connection URL will be automatically generated based on the above fields. To view the URL generated, 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. push the "display secret details" button in the password section.
</span> </span>
</label> </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"> <input id="db-url" name="db-url" type="password"
<span id="db-url-invalid" class="text-xs text-red-500 hidden">Connection URL is incomplete or invalid.</span> 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> </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" onclick="HideModal();" class="bg-blue-500 text-white px-4 py-2 rounded-md">
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 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"> <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>Test Connection</span>
<span id="connection-status" class="w-3 h-3 rounded-full bg-gray-400"></span> <span id="connection-status" class="w-3 h-3 rounded-full bg-gray-400"></span>
</button> </button>
@ -241,4 +232,5 @@
<script src="/v1/web/static/scripts/password.js"></script> <script src="/v1/web/static/scripts/password.js"></script>
<script src="/v1/web/static/scripts/modal.js"></script> <script src="/v1/web/static/scripts/modal.js"></script>
</body> </body>
</html> </html>