forked from github/dataease
feat(数据集): 完成添加SQL的UI。使用umy-ui替换element-ui中el-table,支持超多行、列(上万数据)的table虚拟,grid虚拟,table不会卡顿
This commit is contained in:
parent
105d8a2a5f
commit
8d911cd463
@ -65,4 +65,9 @@ public class DataSetTableController {
|
||||
public Map<String, Object> getPreviewData(@RequestBody DataSetTableRequest dataSetTableRequest) throws Exception {
|
||||
return dataSetTableService.getPreviewData(dataSetTableRequest);
|
||||
}
|
||||
|
||||
@PostMapping("sqlPreview")
|
||||
public Map<String, Object> getSQLPreview(@RequestBody DataSetTableRequest dataSetTableRequest) throws Exception {
|
||||
return dataSetTableService.getSQLPreview(dataSetTableRequest);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import io.dataease.base.domain.Datasource;
|
||||
import io.dataease.datasource.dto.TableFiled;
|
||||
import io.dataease.datasource.request.DatasourceRequest;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -13,6 +14,8 @@ public abstract class DatasourceProvider {
|
||||
|
||||
abstract public List<String[]> getData(DatasourceRequest datasourceRequest) throws Exception;
|
||||
|
||||
abstract public ResultSet getDataResultSet(DatasourceRequest datasourceRequest) throws Exception;
|
||||
|
||||
abstract public List<String> getTables(DatasourceRequest datasourceRequest) throws Exception;
|
||||
|
||||
public List<TableFiled> getTableFileds(DatasourceRequest datasourceRequest) throws Exception{
|
||||
@ -27,4 +30,8 @@ public abstract class DatasourceProvider {
|
||||
|
||||
abstract public List<String[]> getPageData(DatasourceRequest datasourceRequest) throws Exception;
|
||||
|
||||
abstract public List<String[]> fetchResult(ResultSet rs) throws Exception;
|
||||
|
||||
abstract public List<String> fetchResultField(ResultSet rs) throws Exception;
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,13 @@ import io.dataease.datasource.dto.TableFiled;
|
||||
import io.dataease.datasource.request.DatasourceRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.*;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
|
||||
@Service("jdbc")
|
||||
public class JdbcProvider extends DatasourceProvider{
|
||||
public class JdbcProvider extends DatasourceProvider {
|
||||
|
||||
|
||||
@Override
|
||||
@ -25,33 +26,48 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
ResultSet rs = stat.executeQuery(datasourceRequest.getQuery())
|
||||
) {
|
||||
list = fetchResult(rs);
|
||||
} catch (SQLException e){
|
||||
} catch (SQLException e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultSet getDataResultSet(DatasourceRequest datasourceRequest) throws Exception {
|
||||
ResultSet rs;
|
||||
try {
|
||||
Connection connection = getConnection(datasourceRequest);
|
||||
Statement stat = connection.createStatement();
|
||||
rs = stat.executeQuery(datasourceRequest.getQuery());
|
||||
} catch (SQLException e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}
|
||||
return rs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String[]> getPageData(DatasourceRequest datasourceRequest) throws Exception {
|
||||
List<String[]> list = new LinkedList<>();
|
||||
try (
|
||||
Connection connection = getConnection(datasourceRequest);
|
||||
Statement stat = connection.createStatement();
|
||||
ResultSet rs = stat.executeQuery(datasourceRequest.getQuery() + MessageFormat.format(" LIMIT {0}, {1}", (datasourceRequest.getStartPage() -1)*datasourceRequest.getPageSize(), datasourceRequest.getPageSize()))
|
||||
ResultSet rs = stat.executeQuery(datasourceRequest.getQuery() + MessageFormat.format(" LIMIT {0}, {1}", (datasourceRequest.getStartPage() - 1) * datasourceRequest.getPageSize(), datasourceRequest.getPageSize()))
|
||||
) {
|
||||
list = fetchResult(rs);
|
||||
} catch (SQLException e){
|
||||
} catch (SQLException e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
private List<String[]> fetchResult( ResultSet rs) throws Exception{
|
||||
@Override
|
||||
public List<String[]> fetchResult(ResultSet rs) throws Exception {
|
||||
List<String[]> list = new LinkedList<>();
|
||||
ResultSetMetaData metaData = rs.getMetaData();
|
||||
int columnCount = metaData.getColumnCount();
|
||||
@ -73,13 +89,24 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> fetchResultField(ResultSet rs) throws Exception {
|
||||
List<String> fieldList = new ArrayList<>();
|
||||
ResultSetMetaData metaData = rs.getMetaData();
|
||||
int columnCount = metaData.getColumnCount();
|
||||
for (int j = 0; j < columnCount; j++) {
|
||||
fieldList.add(metaData.getColumnName(j + 1));
|
||||
}
|
||||
return fieldList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTables(DatasourceRequest datasourceRequest) throws Exception {
|
||||
List<String> tables = new ArrayList<>();
|
||||
String queryStr = getTablesSql(datasourceRequest);
|
||||
try (Connection con = getConnection(datasourceRequest); Statement ps = con.createStatement()) {
|
||||
ResultSet resultSet = ps.executeQuery(queryStr);
|
||||
while (resultSet.next()){
|
||||
while (resultSet.next()) {
|
||||
tables.add(resultSet.getString(1));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -89,22 +116,22 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TableFiled> getTableFileds(DatasourceRequest datasourceRequest) throws Exception{
|
||||
public List<TableFiled> getTableFileds(DatasourceRequest datasourceRequest) throws Exception {
|
||||
List<TableFiled> list = new LinkedList<>();
|
||||
try (
|
||||
Connection connection = getConnection(datasourceRequest);
|
||||
Connection connection = getConnection(datasourceRequest);
|
||||
) {
|
||||
DatabaseMetaData databaseMetaData = connection.getMetaData();
|
||||
ResultSet resultSet = databaseMetaData.getColumns(null, "%", datasourceRequest.getTable().toUpperCase(), "%");
|
||||
while (resultSet.next()) {
|
||||
String tableName = resultSet.getString("TABLE_NAME");
|
||||
String database = resultSet.getString("TABLE_CAT");
|
||||
if(tableName.equals(datasourceRequest.getTable()) && database.equalsIgnoreCase(getDatabase(datasourceRequest))){
|
||||
if (tableName.equals(datasourceRequest.getTable()) && database.equalsIgnoreCase(getDatabase(datasourceRequest))) {
|
||||
TableFiled tableFiled = new TableFiled();
|
||||
String colName = resultSet.getString("COLUMN_NAME");
|
||||
tableFiled.setFieldName(colName);
|
||||
String remarks = resultSet.getString("REMARKS");
|
||||
if(remarks == null || remarks.equals("")){
|
||||
if (remarks == null || remarks.equals("")) {
|
||||
remarks = colName;
|
||||
}
|
||||
tableFiled.setRemarks(remarks);
|
||||
@ -113,13 +140,15 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
list.add(tableFiled);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e){
|
||||
} catch (SQLException e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new Exception("ERROR:" + e.getMessage(), e);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
@Override
|
||||
public void test(DatasourceRequest datasourceRequest) throws Exception {
|
||||
@ -132,7 +161,7 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
}
|
||||
|
||||
|
||||
public Long count(DatasourceRequest datasourceRequest)throws Exception{
|
||||
public Long count(DatasourceRequest datasourceRequest) throws Exception {
|
||||
try (Connection con = getConnection(datasourceRequest); Statement ps = con.createStatement()) {
|
||||
ResultSet resultSet = ps.executeQuery(datasourceRequest.getQuery());
|
||||
while (resultSet.next()) {
|
||||
@ -150,16 +179,16 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
String driver = null;
|
||||
String jdbcurl = null;
|
||||
DatasourceTypes datasourceType = DatasourceTypes.valueOf(datasourceRequest.getDatasource().getType());
|
||||
switch (datasourceType){
|
||||
switch (datasourceType) {
|
||||
case mysql:
|
||||
MysqlConfigrationDTO mysqlConfigrationDTO = new Gson().fromJson(datasourceRequest.getDatasource().getConfiguration(), MysqlConfigrationDTO.class);
|
||||
MysqlConfigrationDTO mysqlConfigrationDTO = new Gson().fromJson(datasourceRequest.getDatasource().getConfiguration(), MysqlConfigrationDTO.class);
|
||||
username = mysqlConfigrationDTO.getUsername();
|
||||
password = mysqlConfigrationDTO.getPassword();
|
||||
driver = mysqlConfigrationDTO.getDriver();
|
||||
jdbcurl = mysqlConfigrationDTO.getJdbc();
|
||||
break;
|
||||
case sqlServer:
|
||||
SqlServerConfigration sqlServerConfigration= new Gson().fromJson(datasourceRequest.getDatasource().getConfiguration(), SqlServerConfigration.class);
|
||||
SqlServerConfigration sqlServerConfigration = new Gson().fromJson(datasourceRequest.getDatasource().getConfiguration(), SqlServerConfigration.class);
|
||||
username = sqlServerConfigration.getUsername();
|
||||
password = sqlServerConfigration.getPassword();
|
||||
driver = sqlServerConfigration.getDriver();
|
||||
@ -178,7 +207,7 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
return DriverManager.getConnection(jdbcurl, props);
|
||||
}
|
||||
|
||||
private String getDatabase(DatasourceRequest datasourceRequest){
|
||||
private String getDatabase(DatasourceRequest datasourceRequest) {
|
||||
DatasourceTypes datasourceType = DatasourceTypes.valueOf(datasourceRequest.getDatasource().getType());
|
||||
switch (datasourceType) {
|
||||
case mysql:
|
||||
@ -192,9 +221,9 @@ public class JdbcProvider extends DatasourceProvider{
|
||||
}
|
||||
}
|
||||
|
||||
private String getTablesSql(DatasourceRequest datasourceRequest){
|
||||
private String getTablesSql(DatasourceRequest datasourceRequest) {
|
||||
DatasourceTypes datasourceType = DatasourceTypes.valueOf(datasourceRequest.getDatasource().getType());
|
||||
switch (datasourceType){
|
||||
switch (datasourceType) {
|
||||
case mysql:
|
||||
return "show tables;";
|
||||
case sqlServer:
|
||||
|
@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.sql.ResultSet;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@ -150,7 +151,7 @@ public class DataSetTableService {
|
||||
List<DatasetTableField> fields = dataSetTableFieldsService.list(datasetTableField);
|
||||
|
||||
String[] fieldArray = fields.stream().map(DatasetTableField::getOriginName).toArray(String[]::new);
|
||||
datasourceRequest.setQuery(createQuerySQL(ds.getType(), table, fieldArray) + " LIMIT 0,10");
|
||||
datasourceRequest.setQuery(createQuerySQL(ds.getType(), table, fieldArray) + " LIMIT 0,10");// todo limit
|
||||
|
||||
List<String[]> data = new ArrayList<>();
|
||||
try {
|
||||
@ -177,6 +178,35 @@ public class DataSetTableService {
|
||||
return map;
|
||||
}
|
||||
|
||||
public Map<String, Object> getSQLPreview(DataSetTableRequest dataSetTableRequest) throws Exception {
|
||||
Datasource ds = datasourceMapper.selectByPrimaryKey(dataSetTableRequest.getDataSourceId());
|
||||
DatasourceProvider datasourceProvider = ProviderFactory.getProvider(ds.getType());
|
||||
DatasourceRequest datasourceRequest = new DatasourceRequest();
|
||||
datasourceRequest.setDatasource(ds);
|
||||
String sql = new Gson().fromJson(dataSetTableRequest.getInfo(), DataTableInfoDTO.class).getSql();
|
||||
datasourceRequest.setQuery(sql);
|
||||
ResultSet dataResultSet = datasourceProvider.getDataResultSet(datasourceRequest);
|
||||
List<String[]> data = datasourceProvider.fetchResult(dataResultSet);
|
||||
List<String> fields = datasourceProvider.fetchResultField(dataResultSet);
|
||||
|
||||
List<Map<String, Object>> jsonArray = new ArrayList<>();
|
||||
if (CollectionUtils.isNotEmpty(data)) {
|
||||
jsonArray = data.stream().map(ele -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (int i = 0; i < ele.length; i++) {
|
||||
map.put(fields.get(i), ele[i]);
|
||||
}
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("fields", fields);
|
||||
map.put("data", jsonArray);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public List<String[]> getDataSetData(String datasourceId, String table, List<DatasetTableField> fields) {
|
||||
List<String[]> data = new ArrayList<>();
|
||||
Datasource ds = datasourceMapper.selectByPrimaryKey(datasourceId);
|
||||
|
@ -27,6 +27,7 @@
|
||||
"screenfull": "4.2.0",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"umy-ui": "^1.1.6",
|
||||
"vue": "2.6.10",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-i18n": "7.3.2",
|
||||
|
@ -666,7 +666,8 @@ export default {
|
||||
close: '关闭',
|
||||
required: '必填',
|
||||
input_content: '请输入内容',
|
||||
add_sql_table: '添加SQL'
|
||||
add_sql_table: '添加SQL',
|
||||
preview: '预览'
|
||||
},
|
||||
datasource: {
|
||||
create: '新建数据连接',
|
||||
|
@ -28,6 +28,10 @@ import * as echarts from 'echarts'
|
||||
|
||||
Vue.prototype.$echarts = echarts
|
||||
|
||||
import UmyUi from 'umy-ui'
|
||||
import 'umy-ui/lib/theme-chalk/index.css'// 引入样式
|
||||
Vue.use(UmyUi)
|
||||
|
||||
/**
|
||||
* If you don't want to use mock-server
|
||||
* you want to use MockJs for mock api
|
||||
|
@ -3,7 +3,7 @@
|
||||
<el-dropdown trigger="click" size="small">
|
||||
<span class="el-dropdown-link">
|
||||
<el-tag size="small" class="item-axis">
|
||||
{{ item.name }}<span class="summary-span">{{ $t('chart.'+item.summary) }}</span><i class="el-icon-arrow-down el-icon--right" />
|
||||
{{ item.name }}<span v-if="item.summary" class="summary-span">{{ $t('chart.'+item.summary) }}</span><i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-tag>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item icon="el-icon-notebook-2">
|
||||
|
@ -394,8 +394,8 @@ export default {
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 3px 10px;
|
||||
margin: 3px 3px 0 3px;
|
||||
padding: 2px 10px;
|
||||
margin: 2px 2px 0 2px;
|
||||
border: solid 1px #eee;
|
||||
text-align: left;
|
||||
color: #606266;
|
||||
@ -403,15 +403,15 @@ export default {
|
||||
}
|
||||
|
||||
.item-on-move {
|
||||
padding: 3px 10px;
|
||||
margin: 3px 3px 0 3px;
|
||||
padding: 2px 10px;
|
||||
margin: 2px 2px 0 2px;
|
||||
border: solid 1px #eee;
|
||||
text-align: left;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 3px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
|
@ -28,7 +28,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item">
|
||||
<el-input v-model="name" size="mini" placeholder="名称" />
|
||||
<el-input v-model="name" size="mini" :placeholder="$t('commons.name')" />
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item">
|
||||
<el-radio v-model="mode" label="0">{{ $t('dataset.direct_connect') }}</el-radio>
|
||||
<el-radio v-model="mode" label="1">{{ $t('dataset.sync_data') }}</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
@ -45,12 +49,38 @@
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row style="margin-top: 10px;">
|
||||
<el-card class="box-card dataPreview" shadow="never">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>{{ $t('dataset.data_preview') }}</span>
|
||||
<el-button style="float: right; padding: 3px 0" type="text" size="mini" @click="getSQLPreview">{{ $t('dataset.preview') }}</el-button>
|
||||
</div>
|
||||
<div class="text item">
|
||||
<ux-grid
|
||||
ref="plxTable"
|
||||
size="mini"
|
||||
style="width: 100%;"
|
||||
:height="500"
|
||||
:checkbox-config="{highlight: true}"
|
||||
>
|
||||
<ux-table-column
|
||||
v-for="field in fields"
|
||||
:key="field"
|
||||
min-width="200px"
|
||||
:field="field"
|
||||
:title="field"
|
||||
:resizable="true"
|
||||
/>
|
||||
</ux-grid>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-row>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listDatasource } from '@/api/dataset/dataset'
|
||||
import { post, listDatasource } from '@/api/dataset/dataset'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
// 核心样式
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
@ -96,7 +126,10 @@ export default {
|
||||
hintOptions: {
|
||||
completeSingle: true
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [],
|
||||
fields: [],
|
||||
mode: '0'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -115,6 +148,32 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
getSQLPreview() {
|
||||
if (!this.dataSource || this.datasource === '') {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: this.$t('dataset.pls_slc_data_source'),
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
post('/dataset/table/sqlPreview', {
|
||||
dataSourceId: this.dataSource,
|
||||
type: 'sql',
|
||||
info: '{"sql":"' + this.sql + '"}'
|
||||
}).then(response => {
|
||||
this.fields = response.data.fields
|
||||
this.data = response.data.data
|
||||
const datas = this.data
|
||||
this.$refs.plxTable.reloadData(datas)
|
||||
})
|
||||
},
|
||||
|
||||
cancel() {
|
||||
// this.dataReset()
|
||||
this.$emit('switchComponent', { name: '' })
|
||||
},
|
||||
|
||||
showSQL(val) {
|
||||
this.sql = val || ''
|
||||
},
|
||||
@ -125,13 +184,9 @@ export default {
|
||||
// console.log('the editor is focus!', cm)
|
||||
},
|
||||
onCmCodeChange(newCode) {
|
||||
console.log(newCode)
|
||||
// console.log(newCode)
|
||||
this.sql = newCode
|
||||
this.$emit('codeChange', this.sql)
|
||||
},
|
||||
cancel() {
|
||||
// this.dataReset()
|
||||
this.$emit('switchComponent', { name: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,11 +212,23 @@ export default {
|
||||
}
|
||||
|
||||
.codemirror {
|
||||
height: auto;
|
||||
min-height: 100px;
|
||||
height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.codemirror >>> .CodeMirror-scroll {
|
||||
height: auto;
|
||||
min-height: 100px;
|
||||
height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dataPreview>>>.el-card__header{
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.dataPreview>>>.el-card__body{
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
span{
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,19 +1,21 @@
|
||||
<template>
|
||||
<el-col>
|
||||
<el-table
|
||||
<ux-grid
|
||||
ref="plxTable"
|
||||
size="mini"
|
||||
:data="data"
|
||||
border
|
||||
style="width: 100%;"
|
||||
:height="500"
|
||||
:checkbox-config="{highlight: true}"
|
||||
>
|
||||
<el-table-column
|
||||
<ux-table-column
|
||||
v-for="field in fields"
|
||||
:key="field.originName"
|
||||
min-width="200px"
|
||||
:prop="field.originName"
|
||||
:label="field.name"
|
||||
:field="field.originName"
|
||||
:title="field.name"
|
||||
:resizable="true"
|
||||
/>
|
||||
</el-table>
|
||||
</ux-grid>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
@ -29,8 +31,14 @@ export default {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
watch: {},
|
||||
computed: {
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
const datas = this.data
|
||||
this.$refs.plxTable.reloadData(datas)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
},
|
||||
mounted() {
|
||||
|
Loading…
Reference in New Issue
Block a user