Improves visualization data serializers and implements typescript declaration file extraction.
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
} from "@hediet/visualization-core";
|
||||
import { createTreeViewModelFromTreeNodeData } from "../tree-visualizer";
|
||||
import { AstTree, NodeInfo } from "./AstVisualizer";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import LineColumn from "line-column";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
@@ -54,7 +54,7 @@ export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
|
||||
{ inclusive: true }
|
||||
)
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
}).defineAs(visualizationNs("AstTreeNodeItem"))
|
||||
),
|
||||
segment: sOptionalProp(sString()),
|
||||
isMarked: sOptionalProp(sBoolean()),
|
||||
@@ -62,7 +62,7 @@ export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
|
||||
start: sNumber(),
|
||||
length: sNumber(),
|
||||
}),
|
||||
}).defineAs(visualizationNs("TreeNode"))
|
||||
}).defineAs(visualizationNs("AstTreeNode"))
|
||||
);
|
||||
|
||||
export const sAstTree = sOpenObject({
|
||||
@@ -74,7 +74,7 @@ export const sAstTree = sOpenObject({
|
||||
root: sAstTreeNode,
|
||||
text: sString(),
|
||||
fileName: sOptionalProp(sString()),
|
||||
});
|
||||
}).defineAs(visualizationNs("AstTreeVisualizationData"));
|
||||
|
||||
export const astVisualizer = createVisualizer({
|
||||
id: "ast",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
|
||||
import {
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
globalVisualizationFactory,
|
||||
createVisualizer,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../../consts";
|
||||
|
||||
export const graphvizDotVisualizer = createVisualizer({
|
||||
id: "graphviz-dot",
|
||||
@@ -15,7 +16,7 @@ export const graphvizDotVisualizer = createVisualizer({
|
||||
dotGraph: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
}),
|
||||
}).defineAs(visualizationNs("GraphvizDotVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, () => (
|
||||
<GraphvizDotViewer dotCode={data.text} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { GraphvizGraphViewer } from "./GraphvizGraphVisualizer";
|
||||
import { sGraph } from "../sGraph";
|
||||
import {
|
||||
|
||||
@@ -32,4 +32,4 @@ export const sGraph = sOpenObject({
|
||||
kind: sOpenObject({ graph: sLiteral(true) }),
|
||||
nodes: sArrayOf(sGraphNode),
|
||||
edges: sArrayOf(sGraphEdge),
|
||||
}).defineAs(visualizationNs("Graph"));
|
||||
}).defineAs(visualizationNs("GraphVisualizationData"));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
@@ -14,15 +14,18 @@ import {
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const sGrid = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
array: sLiteral(true),
|
||||
grid: sLiteral(true),
|
||||
}),
|
||||
columnLabels: sArrayOf(
|
||||
sOpenObject({
|
||||
label: sOptionalProp(sString(), {}),
|
||||
})
|
||||
columnLabels: sOptionalProp(
|
||||
sArrayOf(
|
||||
sOpenObject({
|
||||
label: sOptionalProp(sString(), {}),
|
||||
})
|
||||
)
|
||||
),
|
||||
rows: sArrayOf(
|
||||
sOpenObject({
|
||||
@@ -55,7 +58,7 @@ export const sGrid = sOpenObject({
|
||||
),
|
||||
{}
|
||||
),
|
||||
});
|
||||
}).defineAs(visualizationNs("GridVisualizationData"));
|
||||
|
||||
export const gridVisualizer = createVisualizer({
|
||||
id: "grid",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { SvgViewer } from "../svg-visualizer/SvgViewer";
|
||||
import { Observer, observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun } from "mobx";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const imageVisualizer = createVisualizer({
|
||||
id: "image",
|
||||
@@ -16,8 +17,10 @@ export const imageVisualizer = createVisualizer({
|
||||
kind: sOpenObject({
|
||||
imagePng: sLiteral(true),
|
||||
}),
|
||||
base64Data: sProp( sString(), { description: "The base 64 encoded PNG representation of the image" }),
|
||||
}),
|
||||
base64Data: sProp(sString(), {
|
||||
description: "The base 64 encoded PNG representation of the image",
|
||||
}),
|
||||
}).defineAs(visualizationNs("ImageVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, () => {
|
||||
return <ImageViewer base64Data={data.base64Data} />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun } from "mobx";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
sOptionalProp,
|
||||
sProp,
|
||||
} from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { getLanguageId, MonacoEditor } from "./MonacoEditor";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const monacoTextVisualizer = createVisualizer({
|
||||
id: "monaco-text",
|
||||
@@ -20,12 +21,12 @@ export const monacoTextVisualizer = createVisualizer({
|
||||
kind: sOpenObject({
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
text: sProp( sString(), { description: "The text to show" }),
|
||||
text: sProp(sString(), { description: "The text to show" }),
|
||||
fileName: sOptionalProp(sString(), {
|
||||
description:
|
||||
"An optional filename that might be used for chosing a syntax highlighter",
|
||||
}),
|
||||
}),
|
||||
}).defineAs(visualizationNs("MonacoTextVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 500 }, theme => {
|
||||
let id = "text";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
import "@finos/perspective-viewer";
|
||||
import "@finos/perspective-viewer-datagrid";
|
||||
import "@finos/perspective-viewer-d3fc";
|
||||
import "@finos/perspective-viewer/themes/all-themes.css";
|
||||
import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
|
||||
|
||||
export class PerspectiveDataViewer extends React.Component<
|
||||
{ data: object[] } & React.HTMLAttributes<HTMLDivElement>
|
||||
> {
|
||||
private readonly nodeRef = React.createRef<HTMLPerspectiveViewerElement>();
|
||||
|
||||
componentDidUpdate() {
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
// TODO: Why timeout?
|
||||
// It does not work without it.
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
this.nodeRef.current!.toggleConfig();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<perspective-viewer
|
||||
class={this.props.className}
|
||||
style={this.props.style}
|
||||
ref={this.nodeRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from "react";
|
||||
import { sOpenObject, sLiteral, sArrayOf, sAny } from "@hediet/semantic-json";
|
||||
import * as React from "react";
|
||||
import { sOpenObject, sLiteral, sArrayOf, sProp } from "@hediet/semantic-json";
|
||||
import { toJS } from "mobx";
|
||||
import "@finos/perspective-viewer";
|
||||
import "@finos/perspective-viewer-datagrid";
|
||||
import "@finos/perspective-viewer-d3fc";
|
||||
import "@finos/perspective-viewer/themes/all-themes.css";
|
||||
import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { makeLazyLoadable } from "../../utils/LazyLoadable";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const sTable = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
table: sLiteral(true),
|
||||
}),
|
||||
rows: sArrayOf(
|
||||
sAny() // object
|
||||
),
|
||||
});
|
||||
rows: sProp(sArrayOf(sOpenObject({})), {
|
||||
description:
|
||||
"An array of objects. The properties of the objects are used as columns.",
|
||||
}),
|
||||
}).defineAs(visualizationNs("TableVisualizationData"));
|
||||
|
||||
const PerspectiveDataViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./PerspectiveDataViewer")).PerspectiveDataViewer
|
||||
);
|
||||
|
||||
export const perspectiveTableVisualizer = createVisualizer({
|
||||
id: "perspective-table",
|
||||
@@ -27,7 +29,7 @@ export const perspectiveTableVisualizer = createVisualizer({
|
||||
serializer: sTable,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, theme => (
|
||||
<PerspectiveDataViewer
|
||||
<PerspectiveDataViewerLazyLoadable
|
||||
className={
|
||||
{
|
||||
light: "perspective-viewer-material",
|
||||
@@ -43,34 +45,4 @@ export const perspectiveTableVisualizer = createVisualizer({
|
||||
)),
|
||||
});
|
||||
|
||||
class PerspectiveDataViewer extends React.Component<
|
||||
{ data: object[] } & React.HTMLAttributes<HTMLDivElement>
|
||||
> {
|
||||
private readonly nodeRef = React.createRef<HTMLPerspectiveViewerElement>();
|
||||
|
||||
componentDidUpdate() {
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
// TODO: Why timeout?
|
||||
// It does not work without it.
|
||||
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
this.nodeRef.current!.toggleConfig();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<perspective-viewer
|
||||
class={this.props.className}
|
||||
style={this.props.style}
|
||||
ref={this.nodeRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
globalVisualizationFactory.addVisualizer(perspectiveTableVisualizer);
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
sOptionalProp,
|
||||
sAny,
|
||||
sProp,
|
||||
sString,
|
||||
sUnion,
|
||||
sNumber,
|
||||
sNull,
|
||||
} from "@hediet/semantic-json";
|
||||
import { makeLazyLoadable } from "../../utils/LazyLoadable";
|
||||
import {
|
||||
@@ -13,11 +17,15 @@ import {
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
const PlotlyViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./PlotlyViewer")).PlotlyViewer
|
||||
);
|
||||
|
||||
const sDatum = sUnion([sString(), sNumber(), sNull()]);
|
||||
const sDatumArr = sUnion([sArrayOf(sDatum), sArrayOf(sArrayOf(sDatum))]);
|
||||
//const x: Plotly.Data;
|
||||
export const plotlyVisualizer = createVisualizer({
|
||||
id: "plotly",
|
||||
name: "plotly",
|
||||
@@ -25,15 +33,83 @@ export const plotlyVisualizer = createVisualizer({
|
||||
kind: sOpenObject({
|
||||
plotly: sLiteral(true),
|
||||
}),
|
||||
data: sProp(sArrayOf(sOpenObject()), {
|
||||
description:
|
||||
"Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)",
|
||||
}),
|
||||
layout: sOptionalProp(sOpenObject(), {
|
||||
description:
|
||||
"Expecting Partial<Plotly.Layout> (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)",
|
||||
}),
|
||||
}),
|
||||
data: sProp(
|
||||
sArrayOf(
|
||||
sOpenObject({
|
||||
text: sOptionalProp(
|
||||
sUnion([sString(), sArrayOf(sString())])
|
||||
),
|
||||
xaxis: sOptionalProp(sString()),
|
||||
yaxis: sOptionalProp(sString()),
|
||||
x: sOptionalProp(sDatumArr),
|
||||
y: sOptionalProp(sDatumArr),
|
||||
z: sOptionalProp(sDatumArr),
|
||||
type: sOptionalProp(
|
||||
sUnion([
|
||||
sLiteral("bar"),
|
||||
sLiteral("box"),
|
||||
sLiteral("candlestick"),
|
||||
sLiteral("choropleth"),
|
||||
sLiteral("contour"),
|
||||
sLiteral("heatmap"),
|
||||
sLiteral("histogram"),
|
||||
sLiteral("indicator"),
|
||||
sLiteral("mesh3d"),
|
||||
sLiteral("ohlc"),
|
||||
sLiteral("parcoords"),
|
||||
sLiteral("pie"),
|
||||
sLiteral("pointcloud"),
|
||||
sLiteral("scatter"),
|
||||
sLiteral("scatter3d"),
|
||||
sLiteral("scattergeo"),
|
||||
sLiteral("scattergl"),
|
||||
sLiteral("scatterpolar"),
|
||||
sLiteral("scatterternary"),
|
||||
sLiteral("sunburst"),
|
||||
sLiteral("surface"),
|
||||
sLiteral("treemap"),
|
||||
sLiteral("waterfall"),
|
||||
sLiteral("funnel"),
|
||||
sLiteral("funnelarea"),
|
||||
sLiteral("scattermapbox"),
|
||||
])
|
||||
),
|
||||
mode: sOptionalProp(
|
||||
sUnion([
|
||||
sLiteral("lines"),
|
||||
sLiteral("markers"),
|
||||
sLiteral("text"),
|
||||
sLiteral("lines+markers"),
|
||||
sLiteral("text+markers"),
|
||||
sLiteral("text+lines"),
|
||||
sLiteral("text+lines+markers"),
|
||||
sLiteral("none"),
|
||||
sLiteral("gauge"),
|
||||
sLiteral("number"),
|
||||
sLiteral("delta"),
|
||||
sLiteral("number+delta"),
|
||||
sLiteral("gauge+number"),
|
||||
sLiteral("gauge+number+delta"),
|
||||
sLiteral("gauge+delta"),
|
||||
])
|
||||
),
|
||||
})
|
||||
),
|
||||
{
|
||||
description:
|
||||
"Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)",
|
||||
}
|
||||
),
|
||||
layout: sOptionalProp(
|
||||
sOpenObject({
|
||||
title: sOptionalProp(sString()),
|
||||
}),
|
||||
{
|
||||
description:
|
||||
"Expecting Partial<Plotly.Layout> (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)",
|
||||
}
|
||||
),
|
||||
}).defineAs(visualizationNs("PlotlyVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, theme => (
|
||||
<PlotlyViewerLazyLoadable
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const simpleTextVisualizer = createVisualizer({
|
||||
id: "simple-text",
|
||||
@@ -14,7 +15,7 @@ export const simpleTextVisualizer = createVisualizer({
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
}),
|
||||
}).defineAs(visualizationNs("SimpleTextVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 100 }, () => (
|
||||
<pre
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { sAny } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import { sAny, sOpenObject } from "@hediet/semantic-json";
|
||||
import * as React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { MonacoEditor } from "../monaco-text-visualizer/MonacoEditor";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export const sourceVisualizer = createVisualizer({
|
||||
id: "source",
|
||||
name: "JSON Source",
|
||||
serializer: sAny(),
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({}),
|
||||
}).defineAs(visualizationNs("SourceVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: -100 }, theme => (
|
||||
<MonacoEditor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json";
|
||||
import { SvgViewer } from "./SvgViewer";
|
||||
import {
|
||||
@@ -15,8 +15,8 @@ export const svgVisualizer = createVisualizer({
|
||||
kind: sOpenObject({
|
||||
svg: sLiteral(true),
|
||||
}),
|
||||
text: sProp( sString(), { description: "The svg content" }),
|
||||
}).defineAs(visualizationNs("SvgData")),
|
||||
text: sProp(sString(), { description: "The svg content" }),
|
||||
}).defineAs(visualizationNs("SvgVisualizationData")),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, () => (
|
||||
<SvgViewer svgContent={data.text} />
|
||||
|
||||
@@ -38,27 +38,39 @@ export interface Item {
|
||||
|
||||
const sTreeNode: Serializer<TreeNode> = sLazy(() =>
|
||||
sOpenObject({
|
||||
children: sProp(sArrayOf(sTreeNode), { description: "The children of this tree-node" }),
|
||||
items: sProp(sArrayOf(
|
||||
sOpenObject({
|
||||
text: sProp(sString(), { description: "The text to show" }),
|
||||
emphasis: sOptionalProp(
|
||||
sUnion(
|
||||
[
|
||||
sLiteral("style1"),
|
||||
sLiteral("style2"),
|
||||
sLiteral("style3"),
|
||||
sString(),
|
||||
],
|
||||
{ inclusive: true }
|
||||
), {
|
||||
description: "The style of the text"
|
||||
}
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
), { description: "The parts that make up the text of this item" }),
|
||||
segment: sOptionalProp(sString(), { description: "If a node is selected, the concatenation of all segment values from root to the selected node is shown to the user." }),
|
||||
isMarked: sOptionalProp(sBoolean(), { description: "Marked nodes are highlighted and scrolled into view on every visualization update." }),
|
||||
children: sProp(sArrayOf(sTreeNode), {
|
||||
description: "The children of this tree-node",
|
||||
}),
|
||||
items: sProp(
|
||||
sArrayOf(
|
||||
sOpenObject({
|
||||
text: sProp(sString(), { description: "The text to show" }),
|
||||
emphasis: sOptionalProp(
|
||||
sUnion(
|
||||
[
|
||||
sLiteral("style1"),
|
||||
sLiteral("style2"),
|
||||
sLiteral("style3"),
|
||||
sString(),
|
||||
],
|
||||
{ inclusive: true }
|
||||
),
|
||||
{
|
||||
description: "The style of the text",
|
||||
}
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
),
|
||||
{ description: "The parts that make up the text of this item" }
|
||||
),
|
||||
segment: sOptionalProp(sString(), {
|
||||
description:
|
||||
"If a node is selected, the concatenation of all segment values from root to the selected node is shown to the user.",
|
||||
}),
|
||||
isMarked: sOptionalProp(sBoolean(), {
|
||||
description:
|
||||
"Marked nodes are highlighted and scrolled into view on every visualization update.",
|
||||
}),
|
||||
}).defineAs(visualizationNs("TreeNode"))
|
||||
);
|
||||
|
||||
@@ -67,7 +79,7 @@ const sTree = sOpenObject({
|
||||
tree: sLiteral(true),
|
||||
}),
|
||||
root: sTreeNode,
|
||||
}).defineAs(visualizationNs("TreeData"));
|
||||
}).defineAs(visualizationNs("TreeVisualizationData"));
|
||||
|
||||
export const treeVisualizer = createVisualizer({
|
||||
id: "tree",
|
||||
|
||||
Reference in New Issue
Block a user