Dashboard Migrations: V2 - legacy services.filter and graphite panel type (#112282)
* migrate to v2 * Fix tests * graphite panels should be auto-migrated * lint * Provisioning: Fix dashboard export to preserve original API version * Add error handling for the edge case where conversion fails but no storedVersion is available.
This commit is contained in:
@@ -946,32 +946,13 @@ func applyPanelAutoMigration(panel map[string]interface{}) {
|
||||
var newType string
|
||||
|
||||
// Graph needs special logic as it can be migrated to multiple panels
|
||||
if panelType == "graph" {
|
||||
// Including graphite which was previously migrated to graph in the schema version 2 migration in DashboardMigrator.ts
|
||||
// but this was a bug because in there graphite was set to graph, but since those migrations run
|
||||
// after PanelModel.restoreModel where autoMigrateFrom is set, this caused the graph migration to be skipped.
|
||||
// And this resulted in a dashboard with invalid panels.
|
||||
if panelType == "graph" || panelType == "graphite" {
|
||||
// Check xaxis mode for special cases
|
||||
if xaxis, ok := panel["xaxis"].(map[string]interface{}); ok {
|
||||
if mode, ok := xaxis["mode"].(string); ok {
|
||||
switch mode {
|
||||
case "series":
|
||||
// Check legend values for bargauge
|
||||
if legend, ok := panel["legend"].(map[string]interface{}); ok {
|
||||
if values, ok := legend["values"].(bool); ok && values {
|
||||
newType = "bargauge"
|
||||
} else {
|
||||
newType = "barchart"
|
||||
}
|
||||
} else {
|
||||
newType = "barchart"
|
||||
}
|
||||
case "histogram":
|
||||
newType = "histogram"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default graph migration to timeseries
|
||||
if newType == "" {
|
||||
newType = "timeseries"
|
||||
}
|
||||
newType = getGraphAutoMigration(panel)
|
||||
} else {
|
||||
// Check autoMigrateAngular mapping
|
||||
autoMigrateAngular := map[string]string{
|
||||
@@ -995,6 +976,36 @@ func applyPanelAutoMigration(panel map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func getGraphAutoMigration(panel map[string]interface{}) string {
|
||||
newType := ""
|
||||
if xaxis, ok := panel["xaxis"].(map[string]interface{}); ok {
|
||||
if mode, ok := xaxis["mode"].(string); ok {
|
||||
switch mode {
|
||||
case "series":
|
||||
// Check legend values for bargauge
|
||||
if legend, ok := panel["legend"].(map[string]interface{}); ok {
|
||||
if values, ok := legend["values"].(bool); ok && values {
|
||||
newType = "bargauge"
|
||||
} else {
|
||||
newType = "barchart"
|
||||
}
|
||||
} else {
|
||||
newType = "barchart"
|
||||
}
|
||||
case "histogram":
|
||||
newType = "histogram"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default graph migration to timeseries
|
||||
if newType == "" {
|
||||
newType = "timeseries"
|
||||
}
|
||||
|
||||
return newType
|
||||
}
|
||||
|
||||
// removeNullValuesRecursively removes null values from nested objects and arrays
|
||||
// This matches the frontend's JSON.stringify/parse behavior
|
||||
func removeNullValuesRecursively(data interface{}) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MIN_VERSION = 2
|
||||
MIN_VERSION = 0
|
||||
LATEST_VERSION = 42
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ type PanelPluginInfo struct {
|
||||
|
||||
func GetMigrations(dsInfoProvider DataSourceInfoProvider) map[int]SchemaVersionMigrationFunc {
|
||||
return map[int]SchemaVersionMigrationFunc{
|
||||
2: V2,
|
||||
3: V3,
|
||||
4: V4,
|
||||
5: V5,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package schemaversion
|
||||
|
||||
import "context"
|
||||
|
||||
// V2 migrates dashboard from schema version 0 or 1 to 2.
|
||||
// This migration handles the legacy services.filter structure.
|
||||
// It matches the frontend DashboardMigrator.ts logic for oldVersion < 2 && finalTargetVersion >= 2.
|
||||
//
|
||||
// Key migrations:
|
||||
// 1. Services filter migration: old.services.filter.time -> dashboard.time
|
||||
// 2. Services filter migration: old.services.filter.list -> dashboard.templating.list
|
||||
//
|
||||
// Example before migration:
|
||||
//
|
||||
// {
|
||||
// "schemaVersion": 1,
|
||||
// "services": {
|
||||
// "filter": {
|
||||
// "time": {"from": "now-1h", "to": "now"},
|
||||
// "list": [{"name": "var1", "type": "query"}]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example after migration:
|
||||
//
|
||||
// {
|
||||
// "schemaVersion": 2,
|
||||
// "time": {"from": "now-1h", "to": "now"},
|
||||
// "templating": {
|
||||
// "list": [{"name": "var1", "type": "query"}]
|
||||
// }
|
||||
// }
|
||||
func V2(_ context.Context, dashboard map[string]interface{}) error {
|
||||
dashboard["schemaVersion"] = 2
|
||||
|
||||
// Migrate services.filter structure
|
||||
migrateServicesFilter(dashboard)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateServicesFilter migrates the legacy services.filter structure
|
||||
func migrateServicesFilter(dashboard map[string]interface{}) {
|
||||
services, ok := dashboard["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
filter, ok := services["filter"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate time property
|
||||
if time, ok := filter["time"]; ok {
|
||||
dashboard["time"] = time
|
||||
}
|
||||
|
||||
// Migrate templating list
|
||||
if list, ok := filter["list"]; ok {
|
||||
if _, exists := dashboard["templating"]; !exists {
|
||||
dashboard["templating"] = map[string]interface{}{}
|
||||
}
|
||||
templating := dashboard["templating"].(map[string]interface{})
|
||||
templating["list"] = list
|
||||
}
|
||||
|
||||
// Remove the services property after migration
|
||||
delete(dashboard, "services")
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package schemaversion_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
)
|
||||
|
||||
func TestV2(t *testing.T) {
|
||||
tests := []migrationTestCase{
|
||||
{
|
||||
name: "services filter migration moves time and templating list",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Services Filter Migration Test",
|
||||
"schemaVersion": 1,
|
||||
"services": map[string]interface{}{
|
||||
"filter": map[string]interface{}{
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
},
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "var1",
|
||||
"type": "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Services Filter Migration Test",
|
||||
"schemaVersion": 2,
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-1h",
|
||||
"to": "now",
|
||||
},
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "var1",
|
||||
"type": "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comprehensive services filter migration",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Comprehensive Services Migration Test",
|
||||
"schemaVersion": 1,
|
||||
"services": map[string]interface{}{
|
||||
"filter": map[string]interface{}{
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "server",
|
||||
"type": "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "graphite",
|
||||
"title": "CPU Usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Comprehensive Services Migration Test",
|
||||
"schemaVersion": 2,
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "server",
|
||||
"type": "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "graphite", // Panel types are not migrated by V2 anymore
|
||||
"title": "CPU Usage",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard with no services or panels",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Minimal Migration Test",
|
||||
"schemaVersion": 1,
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Minimal Migration Test",
|
||||
"schemaVersion": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "services filter with only time (no list)",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Services Time Only Test",
|
||||
"schemaVersion": 1,
|
||||
"services": map[string]interface{}{
|
||||
"filter": map[string]interface{}{
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-2h",
|
||||
"to": "now",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Services Time Only Test",
|
||||
"schemaVersion": 2,
|
||||
"time": map[string]interface{}{
|
||||
"from": "now-2h",
|
||||
"to": "now",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "services filter with only list (no time)",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Services List Only Test",
|
||||
"schemaVersion": 1,
|
||||
"services": map[string]interface{}{
|
||||
"filter": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "env",
|
||||
"type": "custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Services List Only Test",
|
||||
"schemaVersion": 2,
|
||||
"templating": map[string]interface{}{
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "env",
|
||||
"type": "custom",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "panels remain unchanged when no services filter",
|
||||
input: map[string]interface{}{
|
||||
"title": "V2 Panels Unchanged Test",
|
||||
"schemaVersion": 1,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "graphite",
|
||||
"legend": true,
|
||||
"y_format": "short",
|
||||
"y2_format": "bytes",
|
||||
"grid": map[string]interface{}{
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": 2,
|
||||
"type": "graph",
|
||||
"legend": map[string]interface{}{
|
||||
"show": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"title": "V2 Panels Unchanged Test",
|
||||
"schemaVersion": 2,
|
||||
"panels": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": 1,
|
||||
"type": "graphite", // Panel migrations handled by auto-migration
|
||||
"legend": true,
|
||||
"y_format": "short",
|
||||
"y2_format": "bytes",
|
||||
"grid": map[string]interface{}{
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"id": 2,
|
||||
"type": "graph",
|
||||
"legend": map[string]interface{}{
|
||||
"show": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMigrationTests(t, tests, schemaversion.V2)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"title": "V2 Comprehensive Migration Test Dashboard",
|
||||
"services": {
|
||||
"filter": {
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"name": "server",
|
||||
"type": "query",
|
||||
"datasource": "prometheus",
|
||||
"query": "label_values(server)"
|
||||
},
|
||||
{
|
||||
"name": "env",
|
||||
"type": "custom",
|
||||
"options": [
|
||||
{"text": "Production", "value": "prod"},
|
||||
{"text": "Staging", "value": "stage"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "graphite",
|
||||
"title": "CPU Usage",
|
||||
"legend": true,
|
||||
"grid": {
|
||||
"min": 0,
|
||||
"max": 100
|
||||
},
|
||||
"y_format": "percent",
|
||||
"y2_format": "short",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "cpu.usage"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "graph",
|
||||
"title": "Memory Usage",
|
||||
"legend": false,
|
||||
"grid": {
|
||||
"min": 0
|
||||
},
|
||||
"y_format": "bytes",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "memory.usage"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "table",
|
||||
"title": "Server Stats",
|
||||
"legend": true,
|
||||
"grid": {
|
||||
"min": 0,
|
||||
"max": 100
|
||||
},
|
||||
"y_format": "short",
|
||||
"y2_format": "bytes"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "graphite",
|
||||
"title": "Disk I/O",
|
||||
"legend": true,
|
||||
"y2_format": "Bps",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "disk.io"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+168
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"autoMigrateFrom": "graphite",
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"grid": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"id": 1,
|
||||
"legend": true,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A",
|
||||
"target": "cpu.usage"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "timeseries",
|
||||
"y2_format": "short",
|
||||
"y_format": "percent"
|
||||
},
|
||||
{
|
||||
"autoMigrateFrom": "graph",
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"grid": {
|
||||
"min": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": false,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A",
|
||||
"target": "memory.usage"
|
||||
}
|
||||
],
|
||||
"title": "Memory Usage",
|
||||
"type": "timeseries",
|
||||
"y_format": "bytes"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"grid": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"id": 3,
|
||||
"legend": true,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Server Stats",
|
||||
"type": "table",
|
||||
"y2_format": "bytes",
|
||||
"y_format": "short"
|
||||
},
|
||||
{
|
||||
"autoMigrateFrom": "graphite",
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"id": 4,
|
||||
"legend": true,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"apiVersion": "v1",
|
||||
"type": "prometheus",
|
||||
"uid": "default-ds-uid"
|
||||
},
|
||||
"refId": "A",
|
||||
"target": "disk.io"
|
||||
}
|
||||
],
|
||||
"title": "Disk I/O",
|
||||
"type": "timeseries",
|
||||
"y2_format": "Bps"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 42,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": "prometheus",
|
||||
"name": "server",
|
||||
"options": [],
|
||||
"query": "label_values(server)",
|
||||
"refresh": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"name": "env",
|
||||
"options": [
|
||||
{
|
||||
"text": "Production",
|
||||
"value": "prod"
|
||||
},
|
||||
{
|
||||
"text": "Staging",
|
||||
"value": "stage"
|
||||
}
|
||||
],
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "V2 Comprehensive Migration Test Dashboard",
|
||||
"weekStart": ""
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations \u0026 Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"autoMigrateFrom": "graphite",
|
||||
"grid": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"id": 1,
|
||||
"legend": true,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "cpu.usage"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "timeseries",
|
||||
"y2_format": "short",
|
||||
"y_format": "percent"
|
||||
},
|
||||
{
|
||||
"autoMigrateFrom": "graph",
|
||||
"grid": {
|
||||
"min": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": false,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "memory.usage"
|
||||
}
|
||||
],
|
||||
"title": "Memory Usage",
|
||||
"type": "timeseries",
|
||||
"y_format": "bytes"
|
||||
},
|
||||
{
|
||||
"grid": {
|
||||
"max": 100,
|
||||
"min": 0
|
||||
},
|
||||
"id": 3,
|
||||
"legend": true,
|
||||
"title": "Server Stats",
|
||||
"type": "table",
|
||||
"y2_format": "bytes",
|
||||
"y_format": "short"
|
||||
},
|
||||
{
|
||||
"autoMigrateFrom": "graphite",
|
||||
"id": 4,
|
||||
"legend": true,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "disk.io"
|
||||
}
|
||||
],
|
||||
"title": "Disk I/O",
|
||||
"type": "timeseries",
|
||||
"y2_format": "Bps"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 2,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": "prometheus",
|
||||
"name": "server",
|
||||
"options": [],
|
||||
"query": "label_values(server)",
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"name": "env",
|
||||
"options": [
|
||||
{
|
||||
"text": "Production",
|
||||
"value": "prod"
|
||||
},
|
||||
{
|
||||
"text": "Staging",
|
||||
"value": "stage"
|
||||
}
|
||||
],
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "V2 Comprehensive Migration Test Dashboard",
|
||||
"weekStart": ""
|
||||
}
|
||||
Reference in New Issue
Block a user