Dashboard Migrations: v19 - panel links (#109495)
* migrate to v19 * clean up * apply feedback
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MIN_VERSION = 19
|
||||
MIN_VERSION = 18
|
||||
LATEST_VERSION = 41
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ type PanelPluginInfoProvider interface {
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider, panelProvider PanelPluginInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
19: V19,
|
||||
20: V20,
|
||||
21: V21,
|
||||
22: V22,
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package schemaversion
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// V19 migrates panel links to ensure they have proper URL structure and handle legacy properties.
|
||||
// This migration converts legacy panel link properties to the new URL-based format.
|
||||
//
|
||||
// Example before migration:
|
||||
//
|
||||
// "panels": [
|
||||
// {
|
||||
// "links": [
|
||||
// {
|
||||
// "dashboard": "my dashboard",
|
||||
// "keepTime": true,
|
||||
// "includeVars": true,
|
||||
// "params": "customParam"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
// Example after migration:
|
||||
//
|
||||
// "panels": [
|
||||
// {
|
||||
// "links": [
|
||||
// {
|
||||
// "url": "dashboard/db/my-dashboard?$__keepTime&$__includeVars&customParam",
|
||||
// "title": "",
|
||||
// "targetBlank": false
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
func V19(dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 19
|
||||
|
||||
panels, ok := dashboard["panels"].([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range panels {
|
||||
panel, ok := p.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
links, ok := panel["links"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
panel["links"] = upgradePanelLinks(links)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradePanelLinks(links []interface{}) []interface{} {
|
||||
if len(links) == 0 {
|
||||
return links
|
||||
}
|
||||
|
||||
result := []interface{}{}
|
||||
for _, link := range links {
|
||||
linkMap, ok := link.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, upgradePanelLink(linkMap))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func upgradePanelLink(link map[string]interface{}) map[string]interface{} {
|
||||
url := buildPanelLinkURL(link)
|
||||
|
||||
result := map[string]interface{}{
|
||||
"url": url,
|
||||
"title": getStringValue(link, "title"),
|
||||
"targetBlank": getBoolValue(link, "targetBlank"),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildPanelLinkURL builds the URL for a panel link based on legacy properties
|
||||
func buildPanelLinkURL(link map[string]interface{}) string {
|
||||
var url string
|
||||
|
||||
// Check for existing URL first
|
||||
if existingURL, ok := link["url"].(string); ok && existingURL != "" {
|
||||
url = existingURL
|
||||
} else if dashboard, ok := link["dashboard"].(string); ok && dashboard != "" {
|
||||
// Convert dashboard name to slugified URL
|
||||
url = "dashboard/db/" + slugifyForURL(dashboard)
|
||||
} else if dashUri, ok := link["dashUri"].(string); ok && dashUri != "" {
|
||||
url = "dashboard/" + dashUri
|
||||
} else {
|
||||
// Default fallback
|
||||
url = "/"
|
||||
}
|
||||
|
||||
// Add query parameters
|
||||
params := []string{}
|
||||
|
||||
if getBoolValue(link, "keepTime") {
|
||||
params = append(params, "$__url_time_range")
|
||||
}
|
||||
|
||||
if getBoolValue(link, "includeVars") {
|
||||
params = append(params, "$__all_variables")
|
||||
}
|
||||
|
||||
if customParams, ok := link["params"].(string); ok && customParams != "" {
|
||||
params = append(params, customParams)
|
||||
}
|
||||
|
||||
// Append parameters to URL
|
||||
paramUsed := false
|
||||
for _, param := range params {
|
||||
if param != "" {
|
||||
if paramUsed {
|
||||
url += "&"
|
||||
} else {
|
||||
url += "?"
|
||||
paramUsed = true
|
||||
}
|
||||
url += param
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
var reNonWordOrSpace = regexp.MustCompile(`[^a-z0-9_ ]+`)
|
||||
var reSpaces = regexp.MustCompile(` +`)
|
||||
|
||||
// slugifyForURL converts a dashboard name to a URL-friendly slug
|
||||
func slugifyForURL(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
name = reNonWordOrSpace.ReplaceAllString(name, "")
|
||||
name = reSpaces.ReplaceAllString(name, "-")
|
||||
return name
|
||||
}
|
||||
|
||||
func getStringValue(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getBoolValue(m map[string]interface{}, key string) bool {
|
||||
if v, ok := m[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestV19(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "panel with legacy dashboard link gets upgraded to URL format",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 Panel Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"dashboard": "my dashboard",
|
||||
"title": "Dashboard Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 Panel Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "dashboard/db/my-dashboard",
|
||||
"title": "Dashboard Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with legacy dashUri link gets upgraded to URL format",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 DashUri Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"dashUri": "my-dashboard-uid",
|
||||
"title": "DashUri Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 DashUri Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "dashboard/my-dashboard-uid",
|
||||
"title": "DashUri Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with keepTime flag gets upgraded with keepTime parameter",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 KeepTime Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com",
|
||||
"keepTime": true,
|
||||
"title": "KeepTime Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 KeepTime Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com?$__url_time_range",
|
||||
"title": "KeepTime Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with includeVars flag gets upgraded with includeVars parameter",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 IncludeVars Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com",
|
||||
"includeVars": true,
|
||||
"title": "IncludeVars Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 IncludeVars Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com?$__all_variables",
|
||||
"title": "IncludeVars Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with custom params gets upgraded with params in URL",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 Custom Params Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com",
|
||||
"params": "customParam=value",
|
||||
"title": "Custom Params Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 Custom Params Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com?customParam=value",
|
||||
"title": "Custom Params Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with multiple flags and params gets upgraded correctly",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 Complex Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"dashboard": "my dashboard",
|
||||
"keepTime": true,
|
||||
"includeVars": true,
|
||||
"params": "customParam=value",
|
||||
"title": "Complex Link",
|
||||
"targetBlank": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 Complex Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "dashboard/db/my-dashboard?$__url_time_range&$__all_variables&customParam=value",
|
||||
"title": "Complex Link",
|
||||
"targetBlank": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with existing URL and no legacy properties remains unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 Existing URL Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com",
|
||||
"title": "Existing URL Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 Existing URL Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"links": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "http://example.com",
|
||||
"title": "Existing URL Link",
|
||||
"targetBlank": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panel with no links remains unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 No Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 No Links Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard with no panels remains unchanged",
|
||||
input: map[string]interface{}{
|
||||
"title": "V19 No Panels Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V19 No Panels Migration Test Dashboard",
|
||||
"schemaVersion": 19,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V19)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"title": "V19 Panel Links Migration Test Dashboard",
|
||||
"schemaVersion": 18,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "graph",
|
||||
"title": "Panel with legacy dashboard link",
|
||||
"links": [
|
||||
{
|
||||
"dashboard": "my dashboard",
|
||||
"title": "Dashboard Link",
|
||||
"keepTime": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "stat",
|
||||
"title": "Panel with dashUri link",
|
||||
"links": [
|
||||
{
|
||||
"dashUri": "my-dashboard-uid",
|
||||
"title": "DashUri Link",
|
||||
"includeVars": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "table",
|
||||
"title": "Panel with custom params",
|
||||
"links": [
|
||||
{
|
||||
"url": "http://example.com",
|
||||
"params": "customParam=value",
|
||||
"title": "Custom Params Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "timeseries",
|
||||
"title": "Panel with complex link",
|
||||
"links": [
|
||||
{
|
||||
"dashboard": "complex dashboard",
|
||||
"keepTime": true,
|
||||
"includeVars": true,
|
||||
"params": "param1=value1¶m2=value2",
|
||||
"title": "Complex Link",
|
||||
"targetBlank": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "gauge",
|
||||
"title": "Panel with existing URL",
|
||||
"links": [
|
||||
{
|
||||
"url": "http://existing-url.com",
|
||||
"title": "Existing URL Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "singlestat",
|
||||
"title": "Panel with no links"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 1,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Dashboard Link",
|
||||
"url": "dashboard/db/my-dashboard?$__url_time_range"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with legacy dashboard link",
|
||||
"type": "graph"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 2,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "DashUri Link",
|
||||
"url": "dashboard/my-dashboard-uid?$__all_variables"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"justifyMode": "auto",
|
||||
"percentChangeColorMode": "standard",
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with dashUri link",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 3,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Custom Params Link",
|
||||
"url": "http://example.com?customParam=value"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with custom params",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 4,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": true,
|
||||
"title": "Complex Link",
|
||||
"url": "dashboard/db/complex-dashboard?$__url_time_range\u0026$__all_variables\u0026param1=value1\u0026param2=value2"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with complex link",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 5,
|
||||
"links": [
|
||||
{
|
||||
"targetBlank": false,
|
||||
"title": "Existing URL Link",
|
||||
"url": "http://existing-url.com"
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with existing URL",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"colorMode": "none",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "horizontal",
|
||||
"percentChangeColorMode": "standard",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"mean"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showPercentChange": false,
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "1.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel with no links",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"title": "V19 Panel Links Migration Test Dashboard"
|
||||
}
|
||||
Reference in New Issue
Block a user