refactor: 首页重构
This commit is contained in:
parent
88a250d6a6
commit
2f9033c933
6
src/typings/api.d.ts
vendored
6
src/typings/api.d.ts
vendored
@ -200,10 +200,10 @@ declare namespace Api {
|
|||||||
type DashboardLine = {
|
type DashboardLine = {
|
||||||
taskList: TaskList;
|
taskList: TaskList;
|
||||||
rankList: RankList[];
|
rankList: RankList[];
|
||||||
dashboardLineResponseDOList: DashboardLineResponseDOList[];
|
dashboardLineResponseDOList: DashboardLineResponseDO[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DashboardLineResponseDOList = {
|
type DashboardLineResponseDO = {
|
||||||
createDt: string;
|
createDt: string;
|
||||||
total: number;
|
total: number;
|
||||||
} & DashboardLineJob &
|
} & DashboardLineJob &
|
||||||
@ -265,6 +265,8 @@ declare namespace Api {
|
|||||||
*/
|
*/
|
||||||
type DashboardLineMode = 'JOB' | 'WORKFLOW';
|
type DashboardLineMode = 'JOB' | 'WORKFLOW';
|
||||||
|
|
||||||
|
type TaskType = 'JOB' | 'RETRY' | 'WORKFLOW';
|
||||||
|
|
||||||
type DashboardLineParams = {
|
type DashboardLineParams = {
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
type: DashboardLineType;
|
type: DashboardLineType;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { fetchCardCount } from '@/service/api';
|
import { fetchCardCount } from '@/service/api';
|
||||||
import CardData from './modules/card-data.vue';
|
import CardData from './modules/card-data.vue';
|
||||||
import RetryTab from './modules/retry-tab.vue';
|
import TaskTab from './modules/task-tab.vue';
|
||||||
|
|
||||||
const cardCount = ref<Api.Dashboard.CardCount>();
|
const cardCount = ref<Api.Dashboard.CardCount>();
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ getCardData();
|
|||||||
<NSpace vertical :size="16">
|
<NSpace vertical :size="16">
|
||||||
<CardData v-model="cardCount!" />
|
<CardData v-model="cardCount!" />
|
||||||
<NCard :bordered="false" class="card-wrapper p-t-136px 2xl:p-t-0 lg:p-t-36px md:p-t-90px">
|
<NCard :bordered="false" class="card-wrapper p-t-136px 2xl:p-t-0 lg:p-t-36px md:p-t-90px">
|
||||||
<RetryTab v-model="cardCount!" />
|
<TaskTab v-model="cardCount!" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
|
@ -5,16 +5,16 @@ import { useAppStore } from '@/store/modules/app';
|
|||||||
import { useEcharts } from '@/hooks/common/echarts';
|
import { useEcharts } from '@/hooks/common/echarts';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LineRetryChart'
|
name: 'TaskLineChart'
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: number;
|
type?: Api.Dashboard.TaskType;
|
||||||
modelValue: Api.Dashboard.DashboardLine;
|
modelValue: Api.Dashboard.DashboardLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: 0
|
type: 'JOB'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
@ -32,7 +32,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
|
|||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
data:
|
data:
|
||||||
props.type === 0
|
props.type === 'RETRY'
|
||||||
? [
|
? [
|
||||||
$t('common.success'),
|
$t('common.success'),
|
||||||
$t('common.running'),
|
$t('common.running'),
|
||||||
@ -88,7 +88,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#40e9c5',
|
color: '#40e9c5',
|
||||||
name: props.type === 0 ? $t('common.running') : $t('common.fail'),
|
name: props.type === 'RETRY' ? $t('common.running') : $t('common.fail'),
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
stack: 'Total',
|
stack: 'Total',
|
||||||
@ -118,7 +118,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#b686d4',
|
color: '#b686d4',
|
||||||
name: props.type === 0 ? $t('page.manage.retryTask.status.maxRetryTimes') : $t('common.stop'),
|
name: props.type === 'RETRY' ? $t('page.manage.retryTask.status.maxRetryTimes') : $t('common.stop'),
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
stack: 'Total',
|
stack: 'Total',
|
||||||
@ -148,7 +148,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: '#ec6f6f',
|
color: '#ec6f6f',
|
||||||
name: props.type === 0 ? $t('page.manage.retryTask.status.pauseRetry') : $t('common.cancel'),
|
name: props.type === 'RETRY' ? $t('page.manage.retryTask.status.pauseRetry') : $t('common.cancel'),
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
stack: 'Total',
|
stack: 'Total',
|
||||||
@ -190,37 +190,23 @@ const getData = () => {
|
|||||||
|
|
||||||
opts.xAxis.data = props.modelValue?.dashboardLineResponseDOList.map(x => x.createDt);
|
opts.xAxis.data = props.modelValue?.dashboardLineResponseDOList.map(x => x.createDt);
|
||||||
opts.series[0].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
opts.series[0].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
||||||
opts.tabIndex === 0 ? x.successNum : x.success
|
opts.tabIndex === 'RETRY' ? x.successNum : x.success
|
||||||
);
|
);
|
||||||
opts.series[1].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
opts.series[1].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
||||||
opts.tabIndex === 0 ? x.runningNum : x.failNum
|
opts.tabIndex === 'RETRY' ? x.runningNum : x.failNum
|
||||||
);
|
);
|
||||||
opts.series[2].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
opts.series[2].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
||||||
opts.tabIndex === 0 ? x.maxCountNum : x.stop
|
opts.tabIndex === 'RETRY' ? x.maxCountNum : x.stop
|
||||||
);
|
);
|
||||||
opts.series[3].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
opts.series[3].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
|
||||||
opts.tabIndex === 0 ? x.suspendNum : x.cancel
|
opts.tabIndex === 'RETRY' ? x.suspendNum : x.cancel
|
||||||
);
|
);
|
||||||
return opts;
|
return opts;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => appStore.locale,
|
[() => appStore.locale, props],
|
||||||
() => {
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
() => {
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.type,
|
|
||||||
() => {
|
() => {
|
||||||
getData();
|
getData();
|
||||||
},
|
},
|
@ -7,16 +7,16 @@ import { useEcharts } from '@/hooks/common/echarts';
|
|||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'PieRetryChart'
|
name: 'TaskPieChart'
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: number;
|
type?: Api.Dashboard.TaskType;
|
||||||
modelValue: Api.Dashboard.CardCount;
|
modelValue: Api.Dashboard.CardCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: 0
|
type: 'JOB'
|
||||||
});
|
});
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
@ -94,16 +94,7 @@ function updateLocale() {
|
|||||||
opts.tooltip.textStyle.color = originOpts.tooltip.textStyle.color;
|
opts.tooltip.textStyle.color = originOpts.tooltip.textStyle.color;
|
||||||
opts.tooltip.backgroundColor = originOpts.tooltip.backgroundColor;
|
opts.tooltip.backgroundColor = originOpts.tooltip.backgroundColor;
|
||||||
|
|
||||||
if (props.type === 0) {
|
if (props.type === 'JOB') {
|
||||||
const retryTask = props.modelValue.retryTask;
|
|
||||||
opts.series[0].data = [
|
|
||||||
{ name: $t('common.success'), value: retryTask.finishNum / retryTask.totalNum },
|
|
||||||
{ name: $t('common.running'), value: retryTask.runningNum / retryTask.totalNum },
|
|
||||||
{ name: $t('page.manage.retryTask.status.maxRetryTimes'), value: retryTask.maxCountNum / retryTask.totalNum },
|
|
||||||
{ name: $t('page.manage.retryTask.status.pauseRetry'), value: retryTask.suspendNum / retryTask.totalNum }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (props.type === 1) {
|
|
||||||
const jobTask = props.modelValue.jobTask;
|
const jobTask = props.modelValue.jobTask;
|
||||||
opts.series[0].data = [
|
opts.series[0].data = [
|
||||||
{ name: $t('common.success'), value: jobTask.successNum / jobTask.totalNum },
|
{ name: $t('common.success'), value: jobTask.successNum / jobTask.totalNum },
|
||||||
@ -112,7 +103,18 @@ function updateLocale() {
|
|||||||
{ name: $t('common.cancel'), value: jobTask.cancelNum / jobTask.totalNum }
|
{ name: $t('common.cancel'), value: jobTask.cancelNum / jobTask.totalNum }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (props.type === 2) {
|
|
||||||
|
if (props.type === 'RETRY') {
|
||||||
|
const retryTask = props.modelValue.retryTask;
|
||||||
|
opts.series[0].data = [
|
||||||
|
{ name: $t('common.success'), value: retryTask.finishNum / retryTask.totalNum },
|
||||||
|
{ name: $t('common.running'), value: retryTask.runningNum / retryTask.totalNum },
|
||||||
|
{ name: $t('page.manage.retryTask.status.maxRetryTimes'), value: retryTask.maxCountNum / retryTask.totalNum },
|
||||||
|
{ name: $t('page.manage.retryTask.status.pauseRetry'), value: retryTask.suspendNum / retryTask.totalNum }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.type === 'WORKFLOW') {
|
||||||
const workFlowTask = props.modelValue.workFlowTask;
|
const workFlowTask = props.modelValue.workFlowTask;
|
||||||
opts.series[0].data = [
|
opts.series[0].data = [
|
||||||
{ name: $t('common.success'), value: workFlowTask.successNum / workFlowTask.totalNum },
|
{ name: $t('common.success'), value: workFlowTask.successNum / workFlowTask.totalNum },
|
@ -4,11 +4,11 @@ import type { DataTableColumns } from 'naive-ui';
|
|||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { fetchAllGroupName, fetchJobLine, fetchRetryLine } from '@/service/api';
|
import { fetchAllGroupName, fetchJobLine, fetchRetryLine } from '@/service/api';
|
||||||
import LineRetryChart from './line-retry-chart.vue';
|
import TaskLineChart from './task-line-chart.vue';
|
||||||
import PieRetryChart from './pie-retry-chart.vue';
|
import TaskPieChart from './task-pie-chart.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'RetryTab'
|
name: 'TaskTab'
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -17,7 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
|
|
||||||
const type = ref(1);
|
const taskType = ref<Api.Dashboard.TaskType>('JOB');
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||||
const data = ref<Api.Dashboard.DashboardLine>();
|
const data = ref<Api.Dashboard.DashboardLine>();
|
||||||
@ -35,7 +35,7 @@ const formattedValue = ref<[string, string] | null>(
|
|||||||
|
|
||||||
const getData = async () => {
|
const getData = async () => {
|
||||||
const { data: lineData, error } =
|
const { data: lineData, error } =
|
||||||
type.value === 0 ? await fetchRetryLine(tabParams.value) : await fetchJobLine(tabParams.value);
|
taskType.value === 'RETRY' ? await fetchRetryLine(tabParams.value) : await fetchJobLine(tabParams.value);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
data.value = lineData;
|
data.value = lineData;
|
||||||
@ -52,25 +52,17 @@ const getGroupNames = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
|
||||||
() => tabParams.value,
|
|
||||||
() => {
|
|
||||||
getData();
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const onUpdateTab = (value: string) => {
|
const onUpdateTab = (value: string) => {
|
||||||
if (value === 'retryTask') {
|
|
||||||
type.value = 0;
|
|
||||||
tabParams.value.mode = undefined;
|
|
||||||
}
|
|
||||||
if (value === 'jobTask') {
|
if (value === 'jobTask') {
|
||||||
type.value = 1;
|
taskType.value = 'JOB';
|
||||||
tabParams.value.mode = 'JOB';
|
tabParams.value.mode = 'JOB';
|
||||||
}
|
}
|
||||||
|
if (value === 'retryTask') {
|
||||||
|
taskType.value = 'RETRY';
|
||||||
|
tabParams.value.mode = undefined;
|
||||||
|
}
|
||||||
if (value === 'workflow') {
|
if (value === 'workflow') {
|
||||||
type.value = 2;
|
taskType.value = 'WORKFLOW';
|
||||||
tabParams.value.mode = 'WORKFLOW';
|
tabParams.value.mode = 'WORKFLOW';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -130,13 +122,13 @@ const createColumns = (): DataTableColumns<Api.Dashboard.Task> => [
|
|||||||
title: $t('page.home.retryTab.task.run'),
|
title: $t('page.home.retryTab.task.run'),
|
||||||
key: 'run',
|
key: 'run',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: row => <span class="retry-table-number">{row.run}</span>
|
render: row => <span class="task-table-number">{row.run}</span>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t('page.home.retryTab.task.total'),
|
title: $t('page.home.retryTab.task.total'),
|
||||||
key: 'total',
|
key: 'total',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: row => <span class="retry-table-number">{row.total}</span>
|
render: row => <span class="task-table-number">{row.total}</span>
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -150,6 +142,14 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => tabParams.value,
|
||||||
|
() => {
|
||||||
|
getData();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
getData();
|
getData();
|
||||||
getGroupNames();
|
getGroupNames();
|
||||||
</script>
|
</script>
|
||||||
@ -160,20 +160,20 @@ getGroupNames();
|
|||||||
<NTabPane v-for="panel in panels" :key="panel.name" :tab="panel.tab" :name="panel.name">
|
<NTabPane v-for="panel in panels" :key="panel.name" :tab="panel.tab" :name="panel.name">
|
||||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||||
<NGi span="24 s:24 m:16">
|
<NGi span="24 s:24 m:16">
|
||||||
<LineRetryChart v-model="data!" :type="type"></LineRetryChart>
|
<TaskLineChart v-model="data!" :type="taskType" />
|
||||||
</NGi>
|
</NGi>
|
||||||
<NGi span="24 s:24 m:8">
|
<NGi span="24 s:24 m:8">
|
||||||
<div class="retry-tab-rank">
|
<div class="task-tab-rank">
|
||||||
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.rank.title') }}</h4>
|
<h4 class="task-tab-title">{{ $t('page.home.retryTab.rank.title') }}</h4>
|
||||||
<ul class="retry-tab-rank__list">
|
<ul class="task-tab-rank__list">
|
||||||
<li v-for="(item, index) in data?.rankList" :key="index" class="retry-tab-rank__list--item">
|
<li v-for="(item, index) in data?.rankList" :key="index" class="task-tab-rank__list--item">
|
||||||
<span>
|
<span>
|
||||||
<span class="retry-tab-rank__list--index">
|
<span class="task-tab-rank__list--index">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="retry-tab-badge">{{ item.total }}</span>
|
<span class="task-tab-badge">{{ item.total }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -181,7 +181,7 @@ getGroupNames();
|
|||||||
</NGrid>
|
</NGrid>
|
||||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="p-t-16px">
|
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="p-t-16px">
|
||||||
<NGi span="24 s:24 m:16">
|
<NGi span="24 s:24 m:16">
|
||||||
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.task.title') }}</h4>
|
<h4 class="task-tab-title">{{ $t('page.home.retryTab.task.title') }}</h4>
|
||||||
<NDivider />
|
<NDivider />
|
||||||
<NDataTable
|
<NDataTable
|
||||||
min-height="300px"
|
min-height="300px"
|
||||||
@ -193,9 +193,9 @@ getGroupNames();
|
|||||||
/>
|
/>
|
||||||
</NGi>
|
</NGi>
|
||||||
<NGi span="24 s:24 m:8">
|
<NGi span="24 s:24 m:8">
|
||||||
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.pie.title') }}</h4>
|
<h4 class="task-tab-title">{{ $t('page.home.retryTab.pie.title') }}</h4>
|
||||||
<NDivider />
|
<NDivider />
|
||||||
<PieRetryChart v-model="modelValue!" :type="type" />
|
<TaskPieChart v-model="modelValue!" :type="taskType" />
|
||||||
</NGi>
|
</NGi>
|
||||||
</NGrid>
|
</NGrid>
|
||||||
</NTabPane>
|
</NTabPane>
|
||||||
@ -225,7 +225,7 @@ getGroupNames();
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.retry-table-number {
|
.task-table-number {
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
background-color: #f4f4f4;
|
background-color: #f4f4f4;
|
||||||
color: #555;
|
color: #555;
|
||||||
@ -234,19 +234,19 @@ getGroupNames();
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .retry-table-number {
|
.dark .task-table-number {
|
||||||
background: #2c2c2c;
|
background: #2c2c2c;
|
||||||
color: #d6d6d6;
|
color: #d6d6d6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.retry-tab-title {
|
.task-tab-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-tab-badge {
|
.task-tab-badge {
|
||||||
float: right;
|
float: right;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
background-color: #f4f4f4;
|
background-color: #f4f4f4;
|
||||||
@ -256,7 +256,7 @@ getGroupNames();
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-tab-rank {
|
.task-tab-rank {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -303,12 +303,12 @@ getGroupNames();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
.retry-tab-badge {
|
.task-tab-badge {
|
||||||
background: #2c2c2c;
|
background: #2c2c2c;
|
||||||
color: #d6d6d6;
|
color: #d6d6d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-tab-rank {
|
.task-tab-rank {
|
||||||
&__list {
|
&__list {
|
||||||
&--index {
|
&--index {
|
||||||
color: #d6d6d6;
|
color: #d6d6d6;
|
Loading…
Reference in New Issue
Block a user