v0.9.0
This commit is contained in:
@@ -1,90 +1,42 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../Model/Model";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable, computed } from "mobx";
|
||||
import { Popover, Button, Spinner } from "@blueprintjs/core";
|
||||
import classnames = require("classnames");
|
||||
import { Popover } from "@blueprintjs/core";
|
||||
import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { Visualizer } from "./Visualizer";
|
||||
import { ExpressionInput } from "./ExpressionInput";
|
||||
|
||||
@observer
|
||||
export class GUI extends React.Component<{ model: Model }> {
|
||||
@observable text: string = "node";
|
||||
|
||||
@observable expanded = false;
|
||||
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
return (
|
||||
<div className="component-GUI" tabIndex={0}>
|
||||
<div className="part-header">
|
||||
<ExpandButton
|
||||
expanded={this.expanded}
|
||||
setExpanded={v => (this.expanded = v)}
|
||||
/>
|
||||
<div className="part-header-content">
|
||||
<div className="part-header-main">
|
||||
<div className="part-expression-input ">
|
||||
<ExpressionInput model={m} />
|
||||
</div>
|
||||
<div style={{ width: 4 }} />
|
||||
{m.loading ? (
|
||||
<div style={{ padding: "0 4px" }}>
|
||||
<Spinner size={Spinner.SIZE_SMALL} />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="refresh"
|
||||
onClick={() => m.refresh()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="log-in"
|
||||
onClick={() => m.openBrowser()}
|
||||
/>
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="locate"
|
||||
onClick={() => m.useSelectionAsExpression()}
|
||||
/>
|
||||
</div>
|
||||
{this.expanded && <ExpandedOptions model={m} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="part-visualizer">
|
||||
<Visualizer model={m} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderValue(value: string) {
|
||||
return <div>{`${value.length}`}</div>;
|
||||
}
|
||||
}
|
||||
import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Model } from "../Model/Model";
|
||||
import { VisualizationId } from "../Visualizers/Visualizer";
|
||||
|
||||
@observer
|
||||
export class ExpandedOptions extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
let availableExtractors = new Array<DataExtractorInfo>();
|
||||
let selected = -1;
|
||||
let selectedExtractor = -1;
|
||||
if (m.state.kind === "data") {
|
||||
const result = m.state.result;
|
||||
availableExtractors = result.availableExtractors;
|
||||
selected = result.availableExtractors.findIndex(
|
||||
selectedExtractor = result.availableExtractors.findIndex(
|
||||
i => i.id === result.usedExtractor.id
|
||||
);
|
||||
}
|
||||
|
||||
let visualizations = new Array<{
|
||||
label: string;
|
||||
id: VisualizationId;
|
||||
}>();
|
||||
let selectedVisualization = -1;
|
||||
if (m.visualizations) {
|
||||
visualizations = m.visualizations.allVisualizations.map(v => ({
|
||||
label: v.name,
|
||||
id: v.id,
|
||||
}));
|
||||
if (m.visualizations.visualization) {
|
||||
selectedVisualization = visualizations.findIndex(
|
||||
v => v.id === m.visualizations!.visualization!.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: 6 }} />
|
||||
@@ -99,7 +51,7 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
|
||||
|
||||
<NamedSelect
|
||||
name="Extractor"
|
||||
selected={selected}
|
||||
selected={selectedExtractor}
|
||||
options={availableExtractors.map(e => ({
|
||||
label: e.name,
|
||||
id: e.id,
|
||||
@@ -112,7 +64,7 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
|
||||
|
||||
<NamedSelect
|
||||
name="Visualizer"
|
||||
selected={0}
|
||||
selected={selectedVisualization}
|
||||
onSelected={item => {
|
||||
m.setPreferredVisualizationId(item.id);
|
||||
}}
|
||||
@@ -131,23 +83,6 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandButton extends React.Component<{
|
||||
expanded: boolean;
|
||||
setExpanded: (val: boolean) => void;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={() => this.props.setExpanded(!this.props.expanded)}
|
||||
className={classnames(
|
||||
"component-ExpandButton",
|
||||
this.props.expanded && "expanded"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function NamedSelect<T extends SelectableItem>(props: {
|
||||
name: string;
|
||||
selected: number;
|
||||
@@ -169,7 +104,7 @@ interface SelectableItem {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Select<T extends SelectableItem> extends React.Component<{
|
||||
class Select<T extends SelectableItem> extends React.Component<{
|
||||
selected: number;
|
||||
onSelected: (item: T) => void;
|
||||
options: T[];
|
||||
93
extension/ui/src/Components/GUI.tsx
Normal file
93
extension/ui/src/Components/GUI.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React = require("react");
|
||||
import { Button, Spinner } from "@blueprintjs/core";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Model } from "../Model/Model";
|
||||
import { ExpressionInput } from "./ExpressionInput";
|
||||
import { Visualizer } from "./Visualizer";
|
||||
import classnames = require("classnames");
|
||||
import { ExpandedOptions } from "./ExpandedOptions";
|
||||
|
||||
@observer
|
||||
export class GUI extends React.Component<{ model: Model }> {
|
||||
@observable text: string = "node";
|
||||
|
||||
@observable expanded = false;
|
||||
|
||||
render() {
|
||||
const m = this.props.model;
|
||||
return (
|
||||
<div className="component-GUI" tabIndex={0}>
|
||||
<div className="part-header">
|
||||
<ExpandButton
|
||||
expanded={this.expanded}
|
||||
setExpanded={v => (this.expanded = v)}
|
||||
/>
|
||||
<div className="part-header-content">
|
||||
<div className="part-header-main">
|
||||
<div className="part-expression-input ">
|
||||
<ExpressionInput model={m} />
|
||||
</div>
|
||||
<div style={{ width: 4 }} />
|
||||
{m.loading ? (
|
||||
<div style={{ padding: "0 4px" }}>
|
||||
<Spinner size={Spinner.SIZE_SMALL} />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="refresh"
|
||||
onClick={() => m.refresh()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="log-in"
|
||||
onClick={() => m.openBrowser()}
|
||||
/>
|
||||
{/*
|
||||
TODO
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="locate"
|
||||
onClick={() => m.useSelectionAsExpression()}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
{this.expanded && <ExpandedOptions model={m} />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="part-visualizer">
|
||||
<Visualizer model={m} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderValue(value: string) {
|
||||
return <div>{`${value.length}`}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandButton extends React.Component<{
|
||||
expanded: boolean;
|
||||
setExpanded: (val: boolean) => void;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onClick={() => this.props.setExpanded(!this.props.expanded)}
|
||||
className={classnames(
|
||||
"component-ExpandButton",
|
||||
this.props.expanded && "expanded"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ import { MonacoBridge } from "./MonacoBridge";
|
||||
|
||||
declare const window: Window & {
|
||||
serverPort?: number;
|
||||
serverSecret?: string;
|
||||
};
|
||||
|
||||
export class Model {
|
||||
port: number;
|
||||
private port: number;
|
||||
private serverSecret: string;
|
||||
|
||||
@observable expression: string = "";
|
||||
@observable state: DataExtractionState | { kind: "noExpression" } = {
|
||||
@@ -67,6 +69,7 @@ export class Model {
|
||||
constructor() {
|
||||
if (window.serverPort) {
|
||||
this.port = window.serverPort;
|
||||
this.serverSecret = window.serverSecret!;
|
||||
} else {
|
||||
const url = new URL(window.location.href);
|
||||
const portStr = url.searchParams.get("serverPort");
|
||||
@@ -79,6 +82,12 @@ export class Model {
|
||||
if (expr) {
|
||||
this.setExpression(expr);
|
||||
}
|
||||
|
||||
const secret = url.searchParams.get("serverSecret");
|
||||
if (!secret) {
|
||||
throw new Error("Server secret not set.");
|
||||
}
|
||||
this.serverSecret = secret;
|
||||
}
|
||||
|
||||
this.stayConnected();
|
||||
@@ -141,6 +150,11 @@ export class Model {
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
await server.authenticate({ secret: this.serverSecret });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.server = server;
|
||||
|
||||
await stream.onClosed;
|
||||
|
||||
@@ -9,16 +9,12 @@ import {
|
||||
CommonDataTypes,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { createTreeViewModelFromTreeNodeData } from "./TreeVisualizer/Visualizer";
|
||||
import {
|
||||
TreeWithPathView,
|
||||
TreeViewModel,
|
||||
TreeView,
|
||||
} from "./TreeVisualizer/Views";
|
||||
import { TreeWithPathView, TreeViewModel } from "./TreeVisualizer/Views";
|
||||
import React = require("react");
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, trace } from "mobx";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { getLanguageId } from "./MonacoTextVisualizer";
|
||||
import { getLanguageId } from "./TextVisualizer/MonacoTextVisualizer";
|
||||
import LineColumn = require("line-column");
|
||||
|
||||
export class AstVisualizer extends VisualizationProvider {
|
||||
@@ -70,7 +66,7 @@ interface NodeInfo {
|
||||
@observer
|
||||
export class AstTree extends React.Component<{
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
data: CommonDataTypes.AstData;
|
||||
data: CommonDataTypes.Ast;
|
||||
nodeInfoToRange: (info: NodeInfo) => monaco.IRange;
|
||||
}> {
|
||||
render() {
|
||||
|
||||
@@ -3,12 +3,12 @@ import * as React from "react";
|
||||
import { observable, autorun, runInAction } from "mobx";
|
||||
import Viz from "viz.js";
|
||||
import { Module, render } from "viz.js/full.render.js";
|
||||
import { SvgViewer } from "./SvgVisualizer";
|
||||
import { SvgViewer } from "../SvgVisualizer";
|
||||
import {
|
||||
VisualizationProvider,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "./Visualizer";
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
@@ -19,18 +19,18 @@ const viz: any = new Viz({
|
||||
render,
|
||||
});
|
||||
|
||||
export class DotVisualizer extends VisualizationProvider {
|
||||
export class GraphvizDotVisualizer extends VisualizationProvider {
|
||||
getVisualizations(
|
||||
data: ExtractedData,
|
||||
collector: VisualizationCollector
|
||||
): void {
|
||||
if (isCommonDataType(data, { dotGraph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("dot-graph"),
|
||||
name: "Dot Graph",
|
||||
id: asVisualizationId("graphviz-dot"),
|
||||
name: "Graphviz (Dot Data)",
|
||||
priority: 100,
|
||||
render() {
|
||||
return <DotViewer dotCode={data.text} />;
|
||||
return <GraphvizDotViewer dotCode={data.text} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class DotVisualizer extends VisualizationProvider {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DotViewer extends React.Component<{
|
||||
export class GraphvizDotViewer extends React.Component<{
|
||||
dotCode: string;
|
||||
svgRef?: (element: SVGSVGElement | null) => void;
|
||||
}> {
|
||||
@@ -1,29 +1,32 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { DotViewer } from "./DotVisualizer";
|
||||
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
|
||||
import {
|
||||
VisualizationProvider,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "./Visualizer";
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
|
||||
export class DotGraphVisualizer extends VisualizationProvider {
|
||||
export class GraphvizGraphVisualizer extends VisualizationProvider {
|
||||
getVisualizations(
|
||||
data: ExtractedData,
|
||||
collector: VisualizationCollector
|
||||
): void {
|
||||
if (isCommonDataType(data, { graph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("dot-graph"),
|
||||
name: "Dot Graph",
|
||||
id: asVisualizationId("graphviz-graph"),
|
||||
name: "Graphviz",
|
||||
priority: 100,
|
||||
render() {
|
||||
return (
|
||||
<DotGraphViewer edges={data.edges} nodes={data.nodes} />
|
||||
<GraphvizGraphViewer
|
||||
edges={data.edges}
|
||||
nodes={data.nodes}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -32,7 +35,7 @@ export class DotGraphVisualizer extends VisualizationProvider {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DotGraphViewer extends React.Component<{
|
||||
export class GraphvizGraphViewer extends React.Component<{
|
||||
nodes: { id: string; label: string }[];
|
||||
edges: { from: string; to: string; label: string }[];
|
||||
}> {
|
||||
@@ -53,6 +56,6 @@ export class DotGraphViewer extends React.Component<{
|
||||
.join("\n")}
|
||||
}
|
||||
`;
|
||||
return <DotViewer dotCode={dotContent} />;
|
||||
return <GraphvizDotViewer dotCode={dotContent} />;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
VisualizationProvider,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "./Visualizer";
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
NodeGraphData,
|
||||
EdgeGraphData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DataSet, Network } from "vis-network";
|
||||
|
||||
@@ -19,7 +21,7 @@ export class VisJsGraphVisualizer extends VisualizationProvider {
|
||||
if (isCommonDataType(data, { graph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("vis-js-graph"),
|
||||
name: "VisJs Graph",
|
||||
name: "vis.js",
|
||||
priority: 1001,
|
||||
render() {
|
||||
return (
|
||||
@@ -34,34 +36,26 @@ export class VisJsGraphVisualizer extends VisualizationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
interface GraphNode {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface GraphEdge {
|
||||
id?: string;
|
||||
label: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class VisJsGraphViewer extends React.Component<{
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
nodes: NodeGraphData[];
|
||||
edges: EdgeGraphData[];
|
||||
}> {
|
||||
private readonly divRef = React.createRef<HTMLDivElement>();
|
||||
private readonly nodes = new DataSet<{ id: string; label: string }>();
|
||||
private readonly nodes = new DataSet<{
|
||||
id: string;
|
||||
label: string;
|
||||
color?: string;
|
||||
}>();
|
||||
private readonly edges = new DataSet<{
|
||||
id: string;
|
||||
label: string;
|
||||
from: string;
|
||||
to: string;
|
||||
color?: string;
|
||||
}>();
|
||||
|
||||
render() {
|
||||
//const { nodes, edges } = this.props;
|
||||
return <div style={{ height: "100%" }} ref={this.divRef} />;
|
||||
}
|
||||
|
||||
@@ -69,7 +63,11 @@ export class VisJsGraphViewer extends React.Component<{
|
||||
const newNodes = new Set<string>();
|
||||
for (const n of this.props.nodes) {
|
||||
newNodes.add(n.id);
|
||||
this.nodes.update({ id: n.id, label: n.label });
|
||||
this.nodes.update({
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
color: n.color,
|
||||
});
|
||||
}
|
||||
this.nodes.forEach(item => {
|
||||
if (!newNodes.has(item.id)) {
|
||||
@@ -77,11 +75,11 @@ export class VisJsGraphViewer extends React.Component<{
|
||||
}
|
||||
});
|
||||
|
||||
function getIdOfEdge(e: GraphEdge): string {
|
||||
function getIdOfEdge(e: EdgeGraphData): string {
|
||||
if (e.id) {
|
||||
return e.id;
|
||||
}
|
||||
return e.from + "####" + e.to;
|
||||
return e.from + "####" + e.to + "|" + e.label;
|
||||
}
|
||||
|
||||
const newEdges = new Set<string>();
|
||||
@@ -93,6 +91,7 @@ export class VisJsGraphViewer extends React.Component<{
|
||||
label: n.label,
|
||||
from: n.from,
|
||||
to: n.to,
|
||||
color: n.color,
|
||||
});
|
||||
}
|
||||
this.edges.forEach(item => {
|
||||
@@ -121,6 +120,5 @@ export class VisJsGraphViewer extends React.Component<{
|
||||
},
|
||||
};
|
||||
const network = new Network(this.divRef.current!, data, options);
|
||||
//network.stabilize();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
VisualizationProvider,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "./Visualizer";
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
VisualizationProvider,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "./Visualizer";
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TreeWithPathView,
|
||||
} from "./Views";
|
||||
import {
|
||||
CommonDataTypes,
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
TreeNode,
|
||||
@@ -40,7 +39,7 @@ export function createTreeViewModelFromTreeNodeData<TData>(
|
||||
children,
|
||||
node.data
|
||||
);
|
||||
model.isMarked = node.isMarked;
|
||||
model.isMarked = !!node.isMarked;
|
||||
for (const c of children) {
|
||||
c.parent = model;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import "./style.scss";
|
||||
import { ComposedVisualizationProvider } from "./Visualizer";
|
||||
import { TreeVisualizer } from "./TreeVisualizer/Visualizer";
|
||||
import { DotGraphVisualizer } from "./DotGraphVisualizer";
|
||||
import { GraphvizGraphVisualizer } from "./GraphVisualizer/GraphvizGraphVisualizer";
|
||||
import { SvgVisualizer } from "./SvgVisualizer";
|
||||
import { DotVisualizer } from "./DotVisualizer";
|
||||
import { TextVisualizer } from "./TextVisualizer";
|
||||
import { MonacoTextVisualizer } from "./MonacoTextVisualizer";
|
||||
import { GraphvizDotVisualizer } from "./GraphVisualizer/GraphvizDotVisualizer";
|
||||
import { TextVisualizer } from "./TextVisualizer/TextVisualizer";
|
||||
import { MonacoTextVisualizer } from "./TextVisualizer/MonacoTextVisualizer";
|
||||
import { AstVisualizer } from "./AstViewer";
|
||||
import { VisJsGraphVisualizer } from "./VisJsGraphVisualizer";
|
||||
import { VisJsGraphVisualizer } from "./GraphVisualizer/VisJsGraphVisualizer";
|
||||
|
||||
export const knownVisualizations = new ComposedVisualizationProvider([
|
||||
new TreeVisualizer(),
|
||||
new DotGraphVisualizer(),
|
||||
new GraphvizGraphVisualizer(),
|
||||
new VisJsGraphVisualizer(),
|
||||
new SvgVisualizer(),
|
||||
new DotVisualizer(),
|
||||
new GraphvizDotVisualizer(),
|
||||
new TextVisualizer(),
|
||||
new MonacoTextVisualizer(),
|
||||
new AstVisualizer(),
|
||||
|
||||
@@ -2,12 +2,12 @@ import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "./style.scss";
|
||||
import { Model } from "./Model/Model";
|
||||
import Components = require("./Components");
|
||||
import Components = require("./Components/GUI");
|
||||
|
||||
const model = new Model();
|
||||
|
||||
function render(target: HTMLDivElement) {
|
||||
const c = require("./Components") as typeof Components;
|
||||
const c = require("./Components/GUI") as typeof Components;
|
||||
ReactDOM.render(<c.GUI model={model} />, target);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ declare var module: {
|
||||
declare var require: (name: string) => any;
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept("./Components", () => {
|
||||
module.hot.accept("./Components/GUI", () => {
|
||||
render(target);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user