FEAT: Many changes! Tree is almost done
Loading indicator for loading the tree. Tree displays PK, FK, R, and U. Still some bugs with the connections and I want to implement a "connection manager" which the ability to change, edit, and delete connections. PLUS right now the tree view is only supported for PSQL. Need to use different queries for different SQL types. SOME types, like SQLlite, might not support the tree, and I will need a display for that. Plus errors are not handled, just bubbled and logged :|
This commit is contained in:
parent
c1a328ee63
commit
0876222f4c
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Azpect3120/Web-Database-Viewer/internal/model"
|
||||||
"github.com/Azpect3120/Web-Database-Viewer/internal/templates"
|
"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"
|
||||||
@ -38,20 +39,20 @@ 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][]string, error) {
|
func generateTree(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][]string{}, err
|
return map[string][]model.Column{}, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
tree, err := tableList(conn)
|
tree, err := tableList(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string][]string{}, err
|
return map[string][]model.Column{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fillColumns(conn, tree); err != nil {
|
if err := fillColumns(conn, tree); err != nil {
|
||||||
return map[string][]string{}, err
|
return map[string][]model.Column{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree, nil
|
return tree, nil
|
||||||
@ -59,20 +60,20 @@ func generateTree(url string) (map[string][]string, error) {
|
|||||||
|
|
||||||
// Return a map with the keys being the table names and the values
|
// Return a map with the keys being the table names and the values
|
||||||
// being blank which can be later used to store the columns.
|
// being blank which can be later used to store the columns.
|
||||||
func tableList(conn *sql.DB) (map[string][]string, error) {
|
func tableList(conn *sql.DB) (map[string][]model.Column, error) {
|
||||||
rows, err := conn.Query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';")
|
rows, err := conn.Query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string][]string{}, err
|
return map[string][]model.Column{}, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
tree := make(map[string][]string)
|
tree := make(map[string][]model.Column)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var table string
|
var table string
|
||||||
if err := rows.Scan(&table); err != nil {
|
if err := rows.Scan(&table); err != nil {
|
||||||
return map[string][]string{}, err
|
return map[string][]model.Column{}, err
|
||||||
}
|
}
|
||||||
tree[table] = []string{}
|
tree[table] = []model.Column{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree, nil
|
return tree, nil
|
||||||
@ -84,22 +85,90 @@ func tableList(conn *sql.DB) (map[string][]string, error) {
|
|||||||
// For now, the only data stored is the
|
// For now, the only data stored is the
|
||||||
// column name, but in the future this could be expanded to store
|
// column name, but in the future this could be expanded to store
|
||||||
// datatype, constraints, primary keys, relationship, etc.
|
// datatype, constraints, primary keys, relationship, etc.
|
||||||
func fillColumns(conn *sql.DB, tree map[string][]string) error {
|
func fillColumns(conn *sql.DB, tree map[string][]model.Column) error {
|
||||||
|
var pkey string
|
||||||
|
var fkeys []model.ForeignKey
|
||||||
for table := range tree {
|
for table := range tree {
|
||||||
rows, err := conn.Query(fmt.Sprintf("SELECT column_name FROM information_schema.columns WHERE table_name = '%s';", table))
|
unique, err := getUniqueColumns(conn, table)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := conn.Query(fmt.Sprintf("SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = '%s';", table))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer pk.Close()
|
||||||
|
for pk.Next() {
|
||||||
|
if err := pk.Scan(&pkey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fk, err := conn.Query(fmt.Sprintf("SELECT tc.table_schema, tc.table_name, kcu.column_name, ccu.table_schema AS foreign_table_schema, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = '%s';", table))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fk.Close()
|
||||||
|
for fk.Next() {
|
||||||
|
var fkey model.ForeignKey
|
||||||
|
if err := fk.Scan(new(interface{}), new(interface{}), &fkey.Column, new(interface{}), &fkey.ForeignTable, &fkey.ForeignColumn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var column string
|
var column model.Column
|
||||||
if err := rows.Scan(&column); err != nil {
|
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if column.Name == pkey {
|
||||||
|
column.PrimaryKey = true
|
||||||
|
}
|
||||||
|
for _, fkey := range fkeys {
|
||||||
|
if column.Name == fkey.Column {
|
||||||
|
column.ForeignKey = fkey
|
||||||
|
} else {
|
||||||
|
column.ForeignKey = model.ForeignKey{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range unique {
|
||||||
|
if column.Name == u {
|
||||||
|
column.Unique = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tree[table] = append(tree[table], column)
|
tree[table] = append(tree[table], column)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a list of the unique columns in a table
|
||||||
|
func getUniqueColumns(conn *sql.DB, table string) ([]string, error) {
|
||||||
|
var cols []string
|
||||||
|
rows, err := conn.Query(fmt.Sprintf("SELECT kcu.column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.constraint_type = 'UNIQUE' AND kcu.table_name = '%s';", table))
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var col string
|
||||||
|
if err := rows.Scan(&col); err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
cols = append(cols, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cols, nil
|
||||||
|
}
|
||||||
|
|||||||
20
internal/model/tree.go
Normal file
20
internal/model/tree.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
// Column data structure
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
MaxLength sql.NullInt64
|
||||||
|
Nullable string
|
||||||
|
PrimaryKey bool
|
||||||
|
ForeignKey ForeignKey
|
||||||
|
Unique bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForeignKey struct {
|
||||||
|
Column string
|
||||||
|
ForeignTable string
|
||||||
|
ForeignColumn string
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ package templates
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/Azpect3120/Web-Database-Viewer/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tree definition
|
// Tree definition
|
||||||
@ -28,13 +30,14 @@ const FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 spac
|
|||||||
const FIELDS_LIST_CLOSE string = `</ul>`
|
const FIELDS_LIST_CLOSE string = `</ul>`
|
||||||
const FIELD_TEMPLATE string = `
|
const FIELD_TEMPLATE string = `
|
||||||
<li>
|
<li>
|
||||||
<button class="flex items-center" 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"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
|
||||||
</path>
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>%s</span>
|
<span>%s</span>
|
||||||
|
<span class="text-xs ml-auto">%s</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
`
|
`
|
||||||
@ -43,14 +46,14 @@ const FIELD_TEMPLATE string = `
|
|||||||
const PRIMARY_KEY string = `<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>`
|
const PRIMARY_KEY string = `<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>`
|
||||||
|
|
||||||
// Generate the tree based on the database tables and columns
|
// Generate the tree based on the database tables and columns
|
||||||
func TableTree(tree map[string][]string) string {
|
func TableTree(tree map[string][]model.Column) string {
|
||||||
html := TREE_OPEN
|
html := 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(FIELDS_LIST_OPEN, table)
|
||||||
body += fields + generateFields(tree[table]) + FIELDS_LIST_CLOSE
|
body += fields + generateFields(table, tree[table]) + FIELDS_LIST_CLOSE
|
||||||
}
|
}
|
||||||
|
|
||||||
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body)
|
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body)
|
||||||
@ -58,16 +61,16 @@ func TableTree(tree map[string][]string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Using a list of fields, generate the HTML for the fields
|
// Using a list of fields, generate the HTML for the fields
|
||||||
func generateFields(fields []string) 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, field)
|
html += fmt.Sprintf(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][]string) []string {
|
func getSortedKeys(m map[string][]model.Column) []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)
|
||||||
@ -76,3 +79,30 @@ func getSortedKeys(m map[string][]string) []string {
|
|||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the string for the type of column base on the column definition
|
||||||
|
func generateType(col model.Column) string {
|
||||||
|
var str string
|
||||||
|
|
||||||
|
if col.PrimaryKey {
|
||||||
|
str = `<span class="text-yellow-500">PK</span>, `
|
||||||
|
} else if col.ForeignKey.Column != "" {
|
||||||
|
str = fmt.Sprintf(`<span class="text-blue-500">FK: %s(%s)</span>, `, col.ForeignKey.ForeignTable, col.ForeignKey.ForeignColumn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if col.Nullable == "NO" {
|
||||||
|
str += `<span class="text-red-500">R</span>, `
|
||||||
|
}
|
||||||
|
|
||||||
|
if col.Unique {
|
||||||
|
str += `<span class="text-green-500">U</span>, `
|
||||||
|
}
|
||||||
|
|
||||||
|
if col.MaxLength.Valid {
|
||||||
|
str += fmt.Sprintf("%s(%d)", col.Type, col.MaxLength.Int64)
|
||||||
|
} else {
|
||||||
|
str += col.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|||||||
@ -17,9 +17,15 @@ function ToggleFields(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function LoadTableQuery(table) {
|
function LoadTableQuery(table) {
|
||||||
const sql = document.getElementById("sql")
|
const sql = document.getElementById("sql")
|
||||||
sql.value = `SELECT * FROM ${table};`;
|
sql.value = `SELECT * FROM ${table};`;
|
||||||
sql.dispatchEvent(new Event("input", { bubbles: true }));
|
sql.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LoadTableQueryWithFields(table, fields) {
|
||||||
|
const sql = document.getElementById("sql")
|
||||||
|
sql.value = `SELECT ${fields} FROM ${table};`;
|
||||||
|
sql.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -593,6 +593,11 @@ video {
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx-1 {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -601,6 +606,10 @@ video {
|
|||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ml-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-2 {
|
.mr-2 {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -613,14 +622,6 @@ video {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-auto {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-auto {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@ -661,14 +662,14 @@ video {
|
|||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-screen {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-8 {
|
.h-8 {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-screen {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-full {
|
.max-h-full {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
@ -701,14 +702,14 @@ video {
|
|||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-8 {
|
.w-8 {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-full {
|
.min-w-full {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
@ -717,16 +718,6 @@ video {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate-180 {
|
|
||||||
--tw-rotate: 180deg;
|
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
||||||
}
|
|
||||||
|
|
||||||
.rotate-90 {
|
|
||||||
--tw-rotate: 90deg;
|
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
||||||
}
|
|
||||||
|
|
||||||
.transform {
|
.transform {
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
}
|
}
|
||||||
@ -884,16 +875,6 @@ video {
|
|||||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-red-500 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-red-400 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-opacity-50 {
|
.bg-opacity-50 {
|
||||||
--tw-bg-opacity: 0.5;
|
--tw-bg-opacity: 0.5;
|
||||||
}
|
}
|
||||||
@ -910,10 +891,6 @@ video {
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-1 {
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.px-1 {
|
.px-1 {
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
@ -994,6 +971,10 @@ video {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-normal {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -1002,6 +983,11 @@ video {
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-blue-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.text-gray-500 {
|
.text-gray-500 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||||
@ -1017,6 +1003,11 @@ video {
|
|||||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-green-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.text-red-500 {
|
.text-red-500 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||||
@ -1027,6 +1018,11 @@ video {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-yellow-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(234 179 8 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@ -1049,20 +1045,22 @@ video {
|
|||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-300 {
|
.transition-all {
|
||||||
transition-duration: 300ms;
|
transition-property: all;
|
||||||
}
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
.duration-100 {
|
|
||||||
transition-duration: 100ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-150 {
|
.duration-150 {
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.\*\:hidden > * {
|
.duration-300 {
|
||||||
display: none;
|
transition-duration: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ease-in-out {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-100:hover {
|
.hover\:bg-gray-100:hover {
|
||||||
@ -1070,11 +1068,6 @@ video {
|
|||||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-200:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-gray-300:hover {
|
.hover\:bg-gray-300:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<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">
|
||||||
<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">
|
<form hx-post="/v1/api/connections/connect" hx-trigger="change" hx-swap="outerHTML" hx-target="#connected-database" hx-indicator="#table-loading" hx-encoding="multipart/form-data" class="flex items-center justify-end space-x-2 flex-wrap">
|
||||||
<label for="connected-database" class="block text-sm font-medium text-gray-700">Connected Database:</label>
|
<label for="connected-database" class="block text-sm font-medium text-gray-700">Connected Database:</label>
|
||||||
<select hx-get="/v1/web/connections" hx-trigger="load" hx-swap="outerHTML" id="connected-database" name="connected-database" hx-params="none" 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>
|
<select hx-get="/v1/web/connections" hx-trigger="load" hx-swap="outerHTML" id="connected-database" name="connected-database" hx-params="none" 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>
|
||||||
</form>
|
</form>
|
||||||
@ -34,12 +34,16 @@
|
|||||||
<!-- 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"><span id="database-name-tree">database</span> Tables</h2>
|
<h2 class="text-lg font-bold">
|
||||||
|
<span id="database-name-tree">database</span>
|
||||||
|
<span id="table-loading" class="text-sm font-normal mx-1 htmx-indicator duration-300">loading tables...</span>
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
hx-get="/v1/web/connections/tree"
|
hx-get="/v1/web/connections/tree"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
hx-target="#database-table-tree"
|
hx-target="#database-table-tree"
|
||||||
class="hover:bg-gray-100 p-2 rounded-md"
|
class="hover:bg-gray-100 p-2 rounded-md"
|
||||||
|
hx-indicator="#table-loading"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512">
|
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512">
|
||||||
<path d="M400 148l-21.12-24.57A191.43 191.43 0 00240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 00181.07-128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/>
|
<path d="M400 148l-21.12-24.57A191.43 191.43 0 00240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 00181.07-128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/>
|
||||||
@ -47,7 +51,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree" hx-trigger="load" hx-params="none" hx-target="#database-table-tree">
|
<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>
|
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user