FEAT: Enum support all over!
Enum table is generated below the Tables tree. Enums cannot be queried since I don't know what I would want them to query so right now its just for viewing. However, enum types are now displayed properly in the table tree. I still don't know how strong the support will be for other kinds of DBs that aren't PSQL.
This commit is contained in:
parent
8ab00b20d4
commit
83dafb8a08
@ -38,5 +38,5 @@ func ChangeConnection(c *gin.Context) {
|
||||
session.Set("current", name)
|
||||
session.Save()
|
||||
|
||||
c.String(200, templates.ConnectionsList(connections, name)+TableTree(c))
|
||||
c.String(200, templates.ConnectionsList(connections, name)+TableTree(c)+EnumTree(c))
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ func TableTree(c *gin.Context) string {
|
||||
|
||||
url := connections[current]
|
||||
|
||||
tree, err := generateTree(url)
|
||||
tree, err := generateTableTree(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return ""
|
||||
@ -39,7 +39,7 @@ func TableTree(c *gin.Context) string {
|
||||
}
|
||||
|
||||
// Generate the tree of the database tables
|
||||
func generateTree(url string) (map[string][]model.Column, error) {
|
||||
func generateTableTree(url string) (map[string][]model.Column, error) {
|
||||
conn, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return map[string][]model.Column{}, err
|
||||
@ -118,17 +118,23 @@ func fillColumns(conn *sql.DB, tree map[string][]model.Column) error {
|
||||
fkeys = append(fkeys, fkey)
|
||||
}
|
||||
|
||||
rows, err := conn.Query(fmt.Sprintf("SELECT column_name, is_nullable, data_type, character_maximum_length FROM information_schema.columns WHERE table_name = '%s';", table))
|
||||
rows, err := conn.Query(fmt.Sprintf("SELECT c.column_name, c.is_nullable, c.data_type, c.character_maximum_length, t.typname AS enum_type FROM information_schema.columns c JOIN pg_type t ON c.udt_name = t.typname WHERE c.table_name = '%s';", table))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var column model.Column
|
||||
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength); err != nil {
|
||||
var (
|
||||
column model.Column
|
||||
enumType string
|
||||
)
|
||||
if err := rows.Scan(&column.Name, &column.Nullable, &column.Type, &column.MaxLength, &enumType); err != nil {
|
||||
return err
|
||||
}
|
||||
if column.Type == "USER-DEFINED" {
|
||||
column.Type = enumType
|
||||
}
|
||||
if column.Name == pkey {
|
||||
column.PrimaryKey = true
|
||||
}
|
||||
@ -172,3 +178,69 @@ func getUniqueColumns(conn *sql.DB, table string) ([]string, error) {
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
// Generate the tree of the database enums and their values
|
||||
func EnumTree(c *gin.Context) string {
|
||||
session := sessions.Default(c)
|
||||
connections_bytes, ok := session.Get("connections").([]byte)
|
||||
current, ok := session.Get("current").(string)
|
||||
if !ok {
|
||||
fmt.Println("No connections found")
|
||||
return ""
|
||||
}
|
||||
|
||||
var connections map[string]string
|
||||
if err := json.Unmarshal(connections_bytes, &connections); err != nil {
|
||||
fmt.Println(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
url := connections[current]
|
||||
|
||||
enums, err := genereteEnumTree(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return templates.EnumTree(enums)
|
||||
}
|
||||
|
||||
// Generate the tree of the database enums and their values from a
|
||||
// provided connection URL.
|
||||
func genereteEnumTree(url string) (map[string][]string, error) {
|
||||
conn, err := sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
enums, err := enumList(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return enums, nil
|
||||
}
|
||||
|
||||
// Get a list/map of all the enums in the database.
|
||||
// The key is the name of the enum and the value is a slice of the enum values.
|
||||
func enumList(conn *sql.DB) (map[string][]string, error) {
|
||||
rows, err := conn.Query("SELECT t.typname AS enum_name, e.enumlabel AS enum_value FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typcategory = 'E' AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY t.typname, e.enumsortorder;")
|
||||
if err != nil {
|
||||
return map[string][]string{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
enums := make(map[string][]string)
|
||||
for rows.Next() {
|
||||
var enum, value string
|
||||
if err := rows.Scan(&enum, &value); err != nil {
|
||||
return map[string][]string{}, err
|
||||
}
|
||||
|
||||
enums[enum] = append(enums[enum], value)
|
||||
}
|
||||
|
||||
return enums, nil
|
||||
}
|
||||
|
||||
@ -70,9 +70,15 @@ func populate(web, api *gin.RouterGroup) {
|
||||
})
|
||||
api.POST("/connections/connect", database.ChangeConnection)
|
||||
|
||||
web.GET("/connections/tree", func(c *gin.Context) {
|
||||
web.GET("/connections/tree/table", func(c *gin.Context) {
|
||||
c.String(200, database.TableTree(c))
|
||||
})
|
||||
web.GET("/connections/tree/enum", func(c *gin.Context) {
|
||||
c.String(200, database.EnumTree(c))
|
||||
})
|
||||
web.GET("/connections/tree", func(c *gin.Context) {
|
||||
c.String(200, database.TableTree(c)+database.EnumTree(c))
|
||||
})
|
||||
|
||||
web.GET("/query/auto", templates.ToggleQueryType)
|
||||
|
||||
|
||||
@ -7,10 +7,10 @@ import (
|
||||
"github.com/Azpect3120/Web-Database-Viewer/internal/model"
|
||||
)
|
||||
|
||||
// Tree definition
|
||||
const TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">`
|
||||
const TREE_CLOSE string = `</ul>`
|
||||
const TREE_BODY_TEMPLATE string = `<li>%s</li>`
|
||||
// Table tree definition
|
||||
const TABLE_TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2">`
|
||||
const TABLE_TREE_CLOSE string = `</ul>`
|
||||
const TABLE_TREE_BODY_TEMPLATE string = `<li>%s</li>`
|
||||
|
||||
// Table definition
|
||||
const TABLE_TEMPLATE string = `
|
||||
@ -26,9 +26,9 @@ const TABLE_TEMPLATE string = `
|
||||
`
|
||||
|
||||
// Fields definition
|
||||
const FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
|
||||
const FIELDS_LIST_CLOSE string = `</ul>`
|
||||
const FIELD_TEMPLATE string = `
|
||||
const TABLE_FIELDS_LIST_OPEN string = `<ul id="fields-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
|
||||
const TABLE_FIELDS_LIST_CLOSE string = `</ul>`
|
||||
const TABLE_FIELD_TEMPLATE string = `
|
||||
<li>
|
||||
<button onclick="LoadTableQueryWithFields('%s', '%s')" class="flex items-center w-full" title="Select this field">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@ -42,35 +42,63 @@ const FIELD_TEMPLATE string = `
|
||||
</li>
|
||||
`
|
||||
|
||||
// This is not implemented yet
|
||||
const PRIMARY_KEY string = `<span class="h-1.5 w-1.5 bg-yellow-500 rounded-full mx-2" title="Primary Key"></span>`
|
||||
// Enum tree definition
|
||||
const ENUM_TREE_OPEN string = `<ul hx-swap-oob="outerHTML" id="database-enum-tree" class="space-y-2">`
|
||||
const ENUM_TREE_CLOSE string = `</ul>`
|
||||
const ENUM_TREE_BODY_TEMPLATE string = `<li>%s</li>`
|
||||
|
||||
// Enum definition
|
||||
const ENUM_TEMPLATE string = `
|
||||
<button class="w-full text-left text-gray-700 font-medium hover:bg-gray-100 p-2 rounded flex items-center">
|
||||
<svg onclick="ToggleEnumValues('%s');" id="icon-enum-squeeze" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" transform="rotate(-90)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6"></path>
|
||||
</svg>
|
||||
<span class="hover:underline py-1" onclick="ToggleEnumValues('%s');">%s</span>
|
||||
</button>
|
||||
`
|
||||
|
||||
// Enum values definition
|
||||
const ENUM_VALUES_LIST_OPEN string = `<ul id="enum-values-%s" class="hidden ml-6 mt-1 space-y-1 text-gray-600">`
|
||||
const ENUM_VALUES_LIST_CLOSE string = `</ul>`
|
||||
const ENUM_VALUE_TEMPLATE string = `
|
||||
<li>
|
||||
<div class="flex items-center w-full py-2">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7">
|
||||
</path>
|
||||
</svg>
|
||||
<span>%s</span>
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
|
||||
// Generate the tree based on the database tables and columns
|
||||
func TableTree(tree map[string][]model.Column) string {
|
||||
html := TREE_OPEN
|
||||
html := TABLE_TREE_OPEN
|
||||
|
||||
var body string
|
||||
for _, table := range getSortedKeys(tree) {
|
||||
body += fmt.Sprintf(TABLE_TEMPLATE, table, table, table, table, table)
|
||||
fields := fmt.Sprintf(FIELDS_LIST_OPEN, table)
|
||||
body += fields + generateFields(table, tree[table]) + FIELDS_LIST_CLOSE
|
||||
fields := fmt.Sprintf(TABLE_FIELDS_LIST_OPEN, table)
|
||||
body += fields + generateFields(table, tree[table]) + TABLE_FIELDS_LIST_CLOSE
|
||||
}
|
||||
|
||||
html += fmt.Sprintf(TREE_BODY_TEMPLATE, body)
|
||||
return html + TREE_CLOSE
|
||||
html += fmt.Sprintf(TABLE_TREE_BODY_TEMPLATE, body)
|
||||
return html + TABLE_TREE_CLOSE
|
||||
}
|
||||
|
||||
// Using a list of fields, generate the HTML for the fields
|
||||
func generateFields(table string, fields []model.Column) string {
|
||||
var html string
|
||||
for _, field := range fields {
|
||||
html += fmt.Sprintf(FIELD_TEMPLATE, table, field.Name, field.Name, generateType(field))
|
||||
html += fmt.Sprintf(TABLE_FIELD_TEMPLATE, table, field.Name, field.Name, generateType(field))
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
// Return a list of the keys in a map, sorted alphabetically
|
||||
func getSortedKeys(m map[string][]model.Column) []string {
|
||||
func getSortedKeys[T model.Column | string](m map[string][]T) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
@ -106,3 +134,28 @@ func generateType(col model.Column) string {
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Generate the HTML string for the enum tree
|
||||
func EnumTree(enums map[string][]string) string {
|
||||
html := ENUM_TREE_OPEN
|
||||
var body string
|
||||
|
||||
for _, enum := range getSortedKeys(enums) {
|
||||
body += fmt.Sprintf(ENUM_TEMPLATE, enum, enum, enum)
|
||||
valuesList := fmt.Sprintf(ENUM_VALUES_LIST_OPEN, enum)
|
||||
body += valuesList + generateEnumValues(enums[enum]) + ENUM_VALUES_LIST_CLOSE
|
||||
}
|
||||
|
||||
html += fmt.Sprintf(ENUM_TREE_BODY_TEMPLATE, body)
|
||||
|
||||
return html + ENUM_TREE_CLOSE
|
||||
}
|
||||
|
||||
// Convert a list of values into a list of HTML elements
|
||||
func generateEnumValues(values []string) string {
|
||||
var html string
|
||||
for _, value := range values {
|
||||
html += fmt.Sprintf(ENUM_VALUE_TEMPLATE, value)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
*
|
||||
* This file also contains the functions that are used to generate quick queries for the
|
||||
* tables.
|
||||
*
|
||||
* This file also contains the functions that are used to toggle the visibility of the
|
||||
* enum values in the tree view of the tables.
|
||||
*/
|
||||
function ToggleFields(id) {
|
||||
const fields = document.getElementById(`fields-${id}`);
|
||||
@ -29,3 +32,15 @@ function LoadTableQueryWithFields(table, fields) {
|
||||
sql.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
|
||||
function ToggleEnumValues(id) {
|
||||
const enum_values = document.getElementById(`enum-values-${id}`);
|
||||
const button_svg = document.getElementById(`icon-enum-${id}`);
|
||||
|
||||
if (enum_values.classList.contains("hidden")) {
|
||||
enum_values.classList.remove("hidden");
|
||||
button_svg.setAttribute("transform", "rotate(0)");
|
||||
} else {
|
||||
enum_values.classList.add("hidden");
|
||||
button_svg.setAttribute("transform", "rotate(-90)");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -8,11 +8,6 @@
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
|
||||
<link rel="icon" type="image/png" href="/v1/web/assets/favicon.ico">
|
||||
<style>
|
||||
.htmx-request {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
@ -49,11 +44,8 @@
|
||||
<div class="p-4 border-b flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold">
|
||||
<span id="database-name-tree">database</span>
|
||||
<span id="table-loading" class="text-sm font-normal mx-1 htmx-indicator duration-300">loading
|
||||
tables...</span>
|
||||
</h2>
|
||||
<button hx-get="/v1/web/connections/tree" hx-trigger="click" hx-target="#database-table-tree"
|
||||
class="hover:bg-gray-100 p-2 rounded-md" hx-indicator="#table-loading">
|
||||
<button hx-get="/v1/web/connections/tree" hx-trigger="click" hx-swap="none" class="hover:bg-gray-100 p-2 rounded-md" hx-indicator="#table-loading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon h-4 w-4" viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M400 148l-21.12-24.57A191.43 191.43 0 00240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 00181.07-128"
|
||||
@ -63,10 +55,21 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree" hx-trigger="load" hx-params="none"
|
||||
hx-indicator="#table-loading" hx-target="#database-table-tree">
|
||||
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree/table" hx-trigger="load" hx-params="none" hx-indicator="#table-loading" hx-target="#database-table-tree">
|
||||
<div class="w-full flex items-center justify-between border-b pb-4 pt-2">
|
||||
<h2 class="text-lg text-gray-700">Tables</h2>
|
||||
<p id="table-loading" class="text-xs font-light htmx-indicator">Loading...</p>
|
||||
</div>
|
||||
<ul hx-swap-oob="outerHTML" id="database-table-tree" class="space-y-2"></ul>
|
||||
</div>
|
||||
|
||||
<div class="p-4 max-h-full" hx-get="/v1/web/connections/tree/enum" hx-trigger="load" hx-params="none" hx-indicator="#enum-loading" hx-target="#database-enum-tree">
|
||||
<div class="w-full flex items-center justify-between border-b pb-4 pt-2">
|
||||
<h2 class="text-lg text-gray-700">Enums</h2>
|
||||
<p id="enum-loading" class="text-xs font-light htmx-indicator">Loading...</p>
|
||||
</div>
|
||||
<ul hx-swap-oob="outerHTML" id="database-enum-tree" class="space-y-2"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
@ -131,21 +134,27 @@
|
||||
<form id="connection-form" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="db-host" class="block text-sm font-medium text-gray-700">Host</label>
|
||||
<input id="db-host" name="db-host" type="text" placeholder="127.0.0.1" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<input id="db-host" name="db-host" type="text" placeholder="127.0.0.1"
|
||||
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="db-port" class="block text-sm font-medium text-gray-700">Port</label>
|
||||
<input id="db-port" name="db-port" type="text" placeholder="5432" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<input id="db-port" name="db-port" type="text" placeholder="5432"
|
||||
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="db-username" class="block text-sm font-medium text-gray-700">Username</label>
|
||||
<input id="db-username" name="db-username" type="text" placeholder="admin" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<input id="db-username" name="db-username" type="text" placeholder="admin"
|
||||
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="db-password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<div class="relative mt-1">
|
||||
<input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●" class="block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 px-3 py-2 text-gray-500 bg-gray-200 rounded-r-md border border-gray-300" title="Display secret details">
|
||||
<input id="db-password" name="db-password" type="password" placeholder="●●●●●●●●●"
|
||||
class="block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<button type="button" id="togglePassword"
|
||||
class="absolute inset-y-0 right-0 px-3 py-2 text-gray-500 bg-gray-200 rounded-r-md border border-gray-300"
|
||||
title="Display secret details">
|
||||
<svg id="eyeIcon" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s3.5-7 11-7 11 7 11 7-3.5 7-11 7S1 12 1 12z"></path>
|
||||
@ -169,7 +178,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="db-database" class="block text-sm font-medium text-gray-700">Database Name</label>
|
||||
<input id="db-database" name="db-database" type="text" placeholder="master_database" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<input id="db-database" name="db-database" type="text" placeholder="master_database"
|
||||
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label for="db-url" class="block text-sm font-medium text-gray-700">
|
||||
@ -180,7 +190,8 @@
|
||||
will match the database name.
|
||||
</span>
|
||||
</label>
|
||||
<input name="db-conn-name" id="db-conn-name" placeholder="master_database" class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<input name="db-conn-name" id="db-conn-name" placeholder="master_database"
|
||||
class="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label for="db-url" class="block text-sm font-medium text-gray-700">
|
||||
@ -201,15 +212,9 @@
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex items-center space-x-4 mt-4">
|
||||
<button
|
||||
hx-post="/v1/api/connections"
|
||||
hx-trigger="click"
|
||||
hx-target="#connected-database"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#connection-form"
|
||||
hx-on::after-request="HideModal();"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
<button hx-post="/v1/api/connections" hx-trigger="click" hx-target="#connected-database" hx-swap="outerHTML"
|
||||
hx-include="#connection-form" hx-on::after-request="HideModal();"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md">
|
||||
Create Connection
|
||||
</button>
|
||||
<button hx-post="/v1/api/connections/test" hx-trigger="click" hx-swap="outerHTML"
|
||||
@ -234,4 +239,5 @@
|
||||
<script src="/v1/web/static/scripts/modal.js"></script>
|
||||
<script src="/v1/web/static/scripts/tree.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user