CN-572 fix: 修复图表和地图内存泄漏问题
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
import { storageKey } from '@/utils/constants'
|
||||
// 获取初始化时间,默认最近一周
|
||||
Date.prototype.setStart = function () {
|
||||
// 获取初始化时间
|
||||
/* Date.prototype.setStart = function () {
|
||||
this.setHours(0)
|
||||
this.setMinutes(0)
|
||||
this.setSeconds(0)
|
||||
@@ -10,7 +10,7 @@ Date.prototype.setEnd = function () {
|
||||
this.setHours(23)
|
||||
this.setMinutes(59)
|
||||
this.setSeconds(59)
|
||||
}
|
||||
} */
|
||||
// 将时间转化为秒
|
||||
export function getSecond (time) {
|
||||
const ms = getMillisecond(time)
|
||||
|
||||
@@ -122,7 +122,7 @@ export function put (url, params, headers) {
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
resolve(err.response.data)
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
} else if (err.message) {
|
||||
resolve(err.message)
|
||||
}
|
||||
|
||||
@@ -803,7 +803,6 @@ export function scrollToTop (dom, toTop, duration, direction) {
|
||||
if (oldTimestamp !== null) {
|
||||
if (direction === 'up') {
|
||||
scrollY -= totalScrollDistance * (newTimestamp - oldTimestamp) / duration
|
||||
console.info(scrollY)
|
||||
if (scrollY < 0) {
|
||||
dom.scrollTop = 0
|
||||
return
|
||||
|
||||
@@ -167,7 +167,6 @@ export default {
|
||||
methods: {
|
||||
/* 参数 extraParams 额外请求参数 */
|
||||
getChartData (url, extraParams = {}, chartTimeFilter, num) {
|
||||
const vm = this
|
||||
this.loading = true
|
||||
try {
|
||||
if (chartTimeFilter) {
|
||||
@@ -214,7 +213,7 @@ export default {
|
||||
if (Array.isArray(response.data.result)) {
|
||||
response.data.result.forEach(item => {
|
||||
if (item.legend && legendMapping[`${this.entity && this.entity.ip ? 'ip_' : ''}${item.legend}`]) {
|
||||
item.legend = vm.$t(legendMapping[`${this.entity && this.entity.ip ? 'ip_' : ''}${item.legend}`])
|
||||
item.legend = this.$t(legendMapping[`${this.entity && this.entity.ip ? 'ip_' : ''}${item.legend}`])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,24 +3,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import unitConvert from '@/utils/unit-convert'
|
||||
import * as echarts from 'echarts'
|
||||
import { lineToSpace } from '@/utils/tools'
|
||||
import { unitTypes } from '@/utils/constants'
|
||||
import { legendMapping } from '@/views/charts/charts/chart-table-title'
|
||||
import {
|
||||
categoryBar
|
||||
} from '@/views/charts/charts/options/bar'
|
||||
import { getCharBartColor, categoryBarTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import chartEchartMixin from './chart-echart-mixin'
|
||||
|
||||
export default {
|
||||
name: 'ChartCategoryBar',
|
||||
data () {
|
||||
return {
|
||||
myChart: null,
|
||||
chartOption: null
|
||||
}
|
||||
},
|
||||
mixins: [chartEchartMixin],
|
||||
props: {
|
||||
chartInfo: Object,
|
||||
chartData: [Array, Object],
|
||||
@@ -28,7 +20,7 @@ export default {
|
||||
queryParams: Object
|
||||
},
|
||||
methods: {
|
||||
init (id) {
|
||||
initEcharts (id) {
|
||||
const chartParams = this.chartInfo.params
|
||||
const dom = document.getElementById(id)
|
||||
!this.myChart && (this.myChart = echarts.init(dom))
|
||||
@@ -53,7 +45,6 @@ export default {
|
||||
data: []
|
||||
}
|
||||
this.chartData.forEach((r, index) => {
|
||||
// chartParams.direction === 'vertical' ? r.values.map(v => [Number(v[0]) * 1000, Number(v[1]), chartParams.unitType]) : r.values.map(v => [Number(v[1]), Number(v[0]) * 1000, chartParams.unitType])
|
||||
if (chartParams.direction === 'horizontal') {
|
||||
obj.data.push([Number(r.events), r.clientCountry + ' ' + r.clientRegion, chartParams.unitType])
|
||||
} else {
|
||||
@@ -82,14 +73,6 @@ export default {
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
deep: true,
|
||||
handler (n) {
|
||||
this.init(`chart${this.chartInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
}
|
||||
}
|
||||
} else if (this.chartData.length > 1) { // 有多条曲线
|
||||
this.handleLegendClick()// 自定义legend的点击事件
|
||||
this.myChart.on('legendselectchanged', this.handleLegendClick)
|
||||
}
|
||||
}
|
||||
return serie
|
||||
@@ -240,13 +240,10 @@ export default {
|
||||
},
|
||||
|
||||
// 自定义legend的点击事件:此方法只处理多条曲线的情况(单条曲线正常切换legend和曲线)
|
||||
handleLegendClick () {
|
||||
const self = this
|
||||
handleLegendClick (params) {
|
||||
// legend点击事件
|
||||
this.myChart.off('legendselectchanged')
|
||||
this.myChart.on('legendselectchanged', function (params) {
|
||||
const legendNum = Object.keys(params.selected).length
|
||||
const selectedNum = self.getSelectedNum(params)
|
||||
const selectedNum = this.getSelectedNum(params)
|
||||
|
||||
const legendItem = params.selected
|
||||
if (selectedNum === legendNum) { // 点击前:全部曲线高亮
|
||||
@@ -262,14 +259,14 @@ export default {
|
||||
legendItem[name] = true
|
||||
}
|
||||
}
|
||||
self.myChart.setOption({
|
||||
this.myChart.setOption({
|
||||
legend: {
|
||||
selected: legendItem
|
||||
}
|
||||
})
|
||||
|
||||
if (self.getAfterSelectedNum(params) === 1) { // 点击后只有一条曲线高亮
|
||||
const chartParams = self.chartInfo.params
|
||||
if (this.getAfterSelectedNum(params) === 1) { // 点击后只有一条曲线高亮
|
||||
const chartParams = this.chartInfo.params
|
||||
// 多条曲线,且只有一条曲线高亮时,显示P50 P90 分位值,不止一个时隐藏标线
|
||||
let selectedName = ''
|
||||
for (const name in legendItem) {
|
||||
@@ -279,9 +276,9 @@ export default {
|
||||
}
|
||||
|
||||
const serieArray = []
|
||||
self.chartOption.series.forEach((item, i) => {
|
||||
this.chartOption.series.forEach((item, i) => {
|
||||
if (item.name === selectedName) {
|
||||
if (!_.isNaN(parseFloat(self.chartData[i].aggregation.p50)) && !_.isNaN(parseFloat(self.chartData[i].aggregation.p90))) {
|
||||
if (!_.isNaN(parseFloat(this.chartData[i].aggregation.p50)) && !_.isNaN(parseFloat(this.chartData[i].aggregation.p90))) {
|
||||
const markLine = {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
@@ -308,11 +305,11 @@ export default {
|
||||
data: [
|
||||
{
|
||||
name: 'P50',
|
||||
yAxis: self.chartData[i].aggregation.p50
|
||||
yAxis: this.chartData[i].aggregation.p50
|
||||
},
|
||||
{
|
||||
name: 'P90',
|
||||
yAxis: self.chartData[i].aggregation.p90
|
||||
yAxis: this.chartData[i].aggregation.p90
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -321,22 +318,20 @@ export default {
|
||||
}
|
||||
serieArray.push(item)
|
||||
})
|
||||
self.myChart.setOption({
|
||||
this.myChart.setOption({
|
||||
series: serieArray
|
||||
})
|
||||
} else { // 不止一条线高亮时隐藏标线
|
||||
const serieArray = []
|
||||
self.chartOption.series.forEach((item, i) => {
|
||||
this.chartOption.series.forEach((item, i) => {
|
||||
item.markLine = []
|
||||
serieArray.push(item)
|
||||
})
|
||||
self.myChart.setOption({
|
||||
this.myChart.setOption({
|
||||
series: serieArray
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,29 +20,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
ipHostedDomain
|
||||
} from '@/views/charts/charts/options/pie'
|
||||
import chartEchartMixin from './chart-echart-mixin'
|
||||
import { get, post } from '@/utils/http'
|
||||
import { get } from '@/utils/http'
|
||||
import { reverseSortBy } from '@/utils/tools'
|
||||
|
||||
export default {
|
||||
name: 'ChartEchartAppRelateDomain',
|
||||
mixins: [chartEchartMixin],
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
},
|
||||
methods: {
|
||||
initEcharts (id) {
|
||||
this.initDom(id, 2)
|
||||
const chartParams = this.chartInfo.params
|
||||
const domains = this.chartData.toString()
|
||||
// const domains = "office.com,dbank.com"
|
||||
this.$emit('showLoading', true)
|
||||
|
||||
const typeUrl = chartParams.byCategoryUrl.slice(0, chartParams.byCategoryUrl.indexOf('?'))
|
||||
|
||||
@@ -28,10 +28,6 @@ import chartEchartMixin from './chart-echart-mixin'
|
||||
export default {
|
||||
name: 'ChartEchartIpHostedDomain',
|
||||
mixins: [chartEchartMixin],
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
props: {
|
||||
entity: Object
|
||||
},
|
||||
@@ -60,7 +56,6 @@ export default {
|
||||
}
|
||||
})
|
||||
this.chartOption.series[0].data = data
|
||||
// this.myChart.setOption(this.chartOption)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
|
||||
@@ -295,12 +295,6 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statisticsHeight (val) {
|
||||
const dom = document.getElementById(`chart${this.chartInfo.id}`)
|
||||
dom.style.cssText += `height: calc(100% - ${val}px)`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,14 +41,10 @@ import lodash from 'lodash'
|
||||
import { ipOpenPortBar } from '@/views/charts/charts/options/bar'
|
||||
import { getChartColor } from '@/views/charts/charts/tools'
|
||||
import * as echarts from 'echarts'
|
||||
import chartEchartMixin from './chart-echart-mixin'
|
||||
export default {
|
||||
name: 'ChartIpOpenPortBar',
|
||||
setup () {
|
||||
const myChart = null
|
||||
return {
|
||||
myChart
|
||||
}
|
||||
},
|
||||
mixins: [chartEchartMixin],
|
||||
props: {
|
||||
chartInfo: Object,
|
||||
chartData: [Array, Object],
|
||||
@@ -57,7 +53,6 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
chartOption: null,
|
||||
tableData: [],
|
||||
tableKey: [
|
||||
{
|
||||
@@ -81,7 +76,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init (id) {
|
||||
initEcharts (id) {
|
||||
const dom = document.getElementById(id)
|
||||
!this.myChart && (this.myChart = echarts.init(dom))
|
||||
this.tableData = lodash.cloneDeep(this.chartData)
|
||||
@@ -98,29 +93,7 @@ export default {
|
||||
this.chartOption.series[0].data = protocols
|
||||
this.chartOption.xAxis.data = protocols.map(p => p.name)
|
||||
this.myChart.setOption(this.chartOption)
|
||||
},
|
||||
resize () {
|
||||
this.myChart.resize()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.debounceFunc = this.$_.debounce(this.resize, 200)
|
||||
window.addEventListener('resize', this.debounceFunc)
|
||||
},
|
||||
beforeUnmount () {
|
||||
window.removeEventListener('resize', this.debounceFunc)
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
deep: true,
|
||||
handler (n) {
|
||||
this.init(`chart${this.chartInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -174,23 +174,7 @@ export default {
|
||||
|
||||
// 地图点击事件
|
||||
if (this.isMapBlock) {
|
||||
this.polygonSeries.mapPolygons.template.events.on('hit', async ev => {
|
||||
const countryId = ev.target.dataItem.dataContext.id
|
||||
if (countryId) {
|
||||
ev.target.series.chart.zoomToMapObject(ev.target)
|
||||
ev.target.isHover = false
|
||||
const geoData = getGeoData(countryId)
|
||||
if (geoData) {
|
||||
this.countrySeries = shallowRef(this.polygonSeriesFactory())
|
||||
this.countrySeries.geodata = geoData
|
||||
this.polygonSeries.hide()
|
||||
const country = ev.target.dataItem.dataContext.serverCountry
|
||||
const queryParams = { ...this.queryParams, country }
|
||||
const chartData = await getData(replaceUrlPlaceholder(this.chartInfo.params.url, queryParams))
|
||||
this.loadAm4ChartMap(this.countrySeries, null, country, chartData)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.polygonSeries.mapPolygons.template.events.on('hit', this.mapBlockHitEvent)
|
||||
} else if (this.isMapPoint) {
|
||||
/*
|
||||
this.worldImageSeries.mapImages.template.events.on('hit', async ev => {
|
||||
@@ -229,6 +213,23 @@ export default {
|
||||
this.$emit('finishOneMap')
|
||||
}
|
||||
},
|
||||
async mapBlockHitEvent (ev) {
|
||||
const countryId = ev.target.dataItem.dataContext.id
|
||||
if (countryId) {
|
||||
ev.target.series.chart.zoomToMapObject(ev.target)
|
||||
ev.target.isHover = false
|
||||
const geoData = getGeoData(countryId)
|
||||
if (geoData) {
|
||||
this.countrySeries = shallowRef(this.polygonSeriesFactory())
|
||||
this.countrySeries.geodata = geoData
|
||||
this.polygonSeries.hide()
|
||||
const country = ev.target.dataItem.dataContext.serverCountry
|
||||
const queryParams = { ...this.queryParams, country }
|
||||
const chartData = await getData(replaceUrlPlaceholder(this.chartInfo.params.url, queryParams))
|
||||
this.loadAm4ChartMap(this.countrySeries, null, country, chartData)
|
||||
}
|
||||
}
|
||||
},
|
||||
polygonSeriesFactory () {
|
||||
const polygonSeries = this.myChart.series.push(new am4Maps.MapPolygonSeries())
|
||||
polygonSeries.useGeodata = true
|
||||
@@ -528,6 +529,15 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.polygonSeries.mapPolygons.template.events.off('hit', this.mapBlockHitEvent)
|
||||
this.polygonSeries = null
|
||||
this.countrySeries = null
|
||||
this.worldImageSeries = null
|
||||
this.countryImageSeries = null
|
||||
this.myChart && this.myChart.dispose()
|
||||
this.myChart = null
|
||||
},
|
||||
setup (props) {
|
||||
const circleColor = {}
|
||||
circleColor[dnsServerRole.RTDNSM] = {
|
||||
|
||||
@@ -12,8 +12,10 @@ import unitConvert, { valueToRangeValue } from '@/utils/unit-convert'
|
||||
import { unitTypes } from '@/utils/constants'
|
||||
import * as echarts from 'echarts'
|
||||
import { sankey } from './options/sankey'
|
||||
import chartEchartMixin from './chart-echart-mixin'
|
||||
export default {
|
||||
name: 'ChartSanKey',
|
||||
mixins: [chartEchartMixin],
|
||||
props: {
|
||||
chartInfo: Object,
|
||||
chartData: [Array, Object],
|
||||
@@ -25,9 +27,8 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init (id) {
|
||||
initEcharts (id) {
|
||||
const vm = this
|
||||
const chartParams = this.chartInfo.params
|
||||
const entityName = this.entity.ip || this.entity.domain || this.entity.app || this.entity.appName
|
||||
const dom = document.getElementById(id)
|
||||
!this.myChart && (this.myChart = echarts.init(dom))
|
||||
@@ -105,29 +106,7 @@ export default {
|
||||
this.chartOption.series[0].data = data
|
||||
this.chartOption.series[0].links = link
|
||||
this.myChart.setOption(this.chartOption)
|
||||
},
|
||||
resize () {
|
||||
this.myChart.resize()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.debounceFunc = this.$_.debounce(this.resize, 100)
|
||||
window.addEventListener('resize', this.debounceFunc)
|
||||
},
|
||||
beforeUnmount () {
|
||||
window.removeEventListener('resize', this.debounceFunc)
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
deep: true,
|
||||
handler (n) {
|
||||
this.init(`chart${this.chartInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -11,38 +11,24 @@ import { legendMapping } from '@/views/charts/charts/chart-table-title'
|
||||
import {
|
||||
timeBar
|
||||
} from '@/views/charts/charts/options/bar'
|
||||
import { getCharBartColor, timeBarTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import { getCharBartColor } from '@/views/charts/charts/tools'
|
||||
import chartEchartMixin from '@/views/charts/charts/chart-echart-mixin'
|
||||
|
||||
export default {
|
||||
name: 'ChaetTimeBar',
|
||||
setup () {
|
||||
const myChart = null
|
||||
return {
|
||||
myChart
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
chartOption: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
chartInfo: Object,
|
||||
chartData: [Array, Object],
|
||||
resultType: Object,
|
||||
queryParams: Object
|
||||
},
|
||||
mixins: [chartEchartMixin],
|
||||
methods: {
|
||||
init (id) {
|
||||
initEcharts (id) {
|
||||
const chartParams = this.chartInfo.params
|
||||
const dom = document.getElementById(id)
|
||||
!this.myChart && (this.myChart = echarts.init(dom))
|
||||
this.chartOption = this.$_.cloneDeep(timeBar)
|
||||
// this.chartOption.tooltip.trigger = 'item'
|
||||
// this.chartOption.tooltip.formatter = (params) => {
|
||||
// const str = timeBarTooltipFormatter(params, chartParams.direction)
|
||||
// return str
|
||||
// }
|
||||
if (!chartParams.itemColorAlternately) {
|
||||
this.chartOption.series[0].itemStyle.color = function (params) {
|
||||
return getCharBartColor(0)
|
||||
@@ -117,15 +103,6 @@ export default {
|
||||
return allZero
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
deep: true,
|
||||
handler (n) {
|
||||
this.init(`chart${this.chartInfo.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallowRef } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { storageKey, echartsFontSize } from '@/utils/constants'
|
||||
import { storageKey } from '@/utils/constants'
|
||||
import {
|
||||
isEchartsLine,
|
||||
isEchartsPie,
|
||||
@@ -98,7 +98,6 @@ export default {
|
||||
let chartOption = item.getOption()
|
||||
|
||||
if (oldLegendFontSize != echartLegendFontSize) {
|
||||
console.log('updateLegend....')
|
||||
chartOption = {
|
||||
...chartOption,
|
||||
legend: {
|
||||
@@ -110,7 +109,6 @@ export default {
|
||||
}
|
||||
}
|
||||
if (oldLabelFontSize != echartLabelFontSize) {
|
||||
console.log('updateLabel....')
|
||||
const newSeries = []
|
||||
const chartType = chartOption.series[0].type
|
||||
chartOption.series.forEach((series) => {
|
||||
@@ -224,6 +222,16 @@ export default {
|
||||
},
|
||||
beforeUnmount () {
|
||||
window.removeEventListener('resize', this.debounceFunc)
|
||||
if (this.myChart) {
|
||||
this.myChart.off('legendselectchanged')
|
||||
this.myChart.dispose()
|
||||
this.myChart = null
|
||||
}
|
||||
if (this.myChart2) {
|
||||
this.myChart2.off('legendselectchanged')
|
||||
this.myChart2.dispose()
|
||||
this.myChart2 = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -383,7 +383,6 @@ export function categoryVerticalFormatter (params) {
|
||||
return str
|
||||
}
|
||||
export function timeVerticalFormatter (params) {
|
||||
console.log(params)
|
||||
let str = '<div>'
|
||||
params.forEach((item, i) => {
|
||||
const tData = item.data[0]
|
||||
@@ -406,7 +405,6 @@ export function timeVerticalFormatter (params) {
|
||||
return str
|
||||
}
|
||||
export function timeBarTooltipFormatter (params, type) {
|
||||
console.log(params, type)
|
||||
let index1, index0
|
||||
if (type === 'vertical') {
|
||||
index0 = 0
|
||||
@@ -435,7 +433,6 @@ export function timeBarTooltipFormatter (params, type) {
|
||||
return str
|
||||
}
|
||||
export function categoryBarTooltipFormatter (params, type) {
|
||||
console.log(params, type)
|
||||
let index1, index0
|
||||
if (type === 'vertical') {
|
||||
index0 = 0
|
||||
|
||||
@@ -146,7 +146,7 @@ export default {
|
||||
chartDom.innerHTML = '<span style="padding-left:5px;">-</span>'
|
||||
}
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
const chartDom = document.getElementById(`detectionMetricChartApp${this.detection.appName}`)
|
||||
chartDom.innerHTML = '<span style="padding-left:5px;">-</span>'
|
||||
}).finally(() => {
|
||||
|
||||
@@ -162,7 +162,6 @@ export default {
|
||||
|
||||
this.metricList.sort(reverseSortBy(0))// 将返回的数据按时间降序排序,方便找到实线和虚线的交点
|
||||
// let endIndex = (this.metricList). findIndex ((item) => item[0] <= 1435781434781 );
|
||||
console.log(this.detection)
|
||||
let endIndex = (this.metricList).findIndex((item) => item[0] <= this.detection.endTime)
|
||||
endIndex = this.metricList.length - endIndex
|
||||
|
||||
|
||||
Reference in New Issue
Block a user