feat: 跟踪任务查看pcap内容信息

This commit is contained in:
TsMask
2024-09-23 17:24:02 +08:00
parent 776e9c5837
commit f7273457e9
5 changed files with 346 additions and 26 deletions

View File

@@ -65,6 +65,21 @@ export async function delTraceTask(ids: string) {
}); });
} }
/**
* 跟踪任务文件
* @param query 对象
* @returns object
*/
export function filePullTask(traceId: string) {
return request({
url: '/trace/task/filePull',
method: 'get',
params: { traceId },
responseType: 'blob',
timeout: 60_000,
});
}
/** /**
* 获取网元跟踪接口列表 * 获取网元跟踪接口列表
* @returns object * @returns object

View File

@@ -1126,9 +1126,7 @@ export default {
stopNotRun: "{title} not running", stopNotRun: "{title} not running",
}, },
task: { task: {
neTypePlease: 'Query network element type', traceId: 'Tracing No',
neType: 'NE Type',
neID: 'NE ID',
trackType: 'Tracing Type', trackType: 'Tracing Type',
trackTypePlease: 'Please select a tracing type', trackTypePlease: 'Please select a tracing type',
creater: 'Created by', creater: 'Created by',
@@ -1165,6 +1163,7 @@ export default {
delTaskTip: 'Are you sure to delete the data item with record ID {id} ?', delTaskTip: 'Are you sure to delete the data item with record ID {id} ?',
stopTask: 'Successful cessation of tasks {id}', stopTask: 'Successful cessation of tasks {id}',
stopTaskTip: 'Confirm stopping the task with record ID {id} ?', stopTaskTip: 'Confirm stopping the task with record ID {id} ?',
pcapView: "Tracking Data Analysis",
traceFile: "Tracking File", traceFile: "Tracking File",
errMsg: "Error Message", errMsg: "Error Message",
imsiORmsisdn: "imsi or msisdn is null, cannot start task", imsiORmsisdn: "imsi or msisdn is null, cannot start task",

View File

@@ -1126,9 +1126,7 @@ export default {
stopNotRun: "{title} 任务未运行", stopNotRun: "{title} 任务未运行",
}, },
task: { task: {
neTypePlease: '请选择网元类型', traceId: '跟踪编号',
neType: '网元类型',
neID: '网元内部标识',
trackType: '跟踪类型', trackType: '跟踪类型',
trackTypePlease: '请选择跟踪类型', trackTypePlease: '请选择跟踪类型',
creater: '创建人', creater: '创建人',
@@ -1165,6 +1163,7 @@ export default {
delTaskTip: '确认删除记录编号为 {id} 的数据项?', delTaskTip: '确认删除记录编号为 {id} 的数据项?',
stopTask: '成功停止任务 {id}', stopTask: '成功停止任务 {id}',
stopTaskTip: '确认停止记录编号为 {id} 的任务?', stopTaskTip: '确认停止记录编号为 {id} 的任务?',
pcapView: "跟踪数据分析",
traceFile: "跟踪文件", traceFile: "跟踪文件",
errMsg: "错误信息", errMsg: "错误信息",
imsiORmsisdn: "imsi 或 msisdn 是空值,不能开始任务", imsiORmsisdn: "imsi 或 msisdn 是空值,不能开始任务",

View File

@@ -0,0 +1,287 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { PageContainer } from 'antdv-pro-layout';
import DissectionTree from '../tshark/components/DissectionTree.vue';
import DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from '../tshark/hooks/usePCAP';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import useI18n from '@/hooks/useI18n';
import useTabsStore from '@/store/modules/tabs';
const route = useRoute();
const router = useRouter();
const tabsStore = useTabsStore();
const ws = new WS();
const { t } = useI18n();
const {
state,
handleSelectedTreeEntry,
handleSelectedFindSelection,
handleSelectedFrame,
handleScrollBottom,
handleFilterFrames,
handleLoadFile,
} = usePCAP();
/**跟踪编号 */
const traceId = ref<string>(route.query.traceId as string);
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**获取PCAP文件 */
function fnFilePCAP() {
filePullTask(traceId.value).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
handleLoadFile(res.data);
}
});
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 建联时发送请求
if (!requestId && data.clientId) {
fnFilePCAP();
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
if (data.groupId === `2_${traceId.value}`) {
fnFilePCAP();
}
}
/**建立WS连接 */
function fnWS() {
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 跟踪任务PCAP文件 (GroupID:2_traceId)
*/
subGroupID: `2_${traceId.value}`,
},
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
watch(
() => state.initialized,
v => {
v && fnWS();
}
);
onBeforeUnmount(() => {
ws.close();
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:loading="!state.initialized"
:body-style="{ padding: '12px' }"
>
<div class="toolbar">
<a-space :size="8" class="toolbar-oper">
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
{{ t('common.close') }}
</a-button>
<span>
跟踪编号
<strong>{{ traceId }}</strong>
</span>
</a-space>
<div class="toolbar-info">
<a-tag color="green" v-show="!!state.currentFilter">
{{ state.currentFilter }}
</a-tag>
<span> Matched Frame: {{ state.totalFrames }} </span>
</div>
<!-- 包信息 -->
<a-popover
trigger="click"
placement="bottomLeft"
v-if="state.summary.filename"
>
<template #content>
<div class="summary">
<div class="summary-item">
<span>Type:</span>
<span>{{ state.summary.file_type }}</span>
</div>
<div class="summary-item">
<span>Encapsulation:</span>
<span>{{ state.summary.file_encap_type }}</span>
</div>
<div class="summary-item">
<span>Packets:</span>
<span>{{ state.summary.packet_count }}</span>
</div>
<div class="summary-item">
<span>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</div>
<!-- 包数据表过滤 -->
<a-input-group compact>
<a-input
v-model:value="state.filter"
placeholder="display filter, example: tcp"
:allow-clear="true"
style="width: calc(100% - 100px)"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
<a-button
type="primary"
html-type="submit"
style="width: 100px"
@click="handleFilterFrames"
>
Filter
</a-button>
</a-input-group>
<a-alert
:message="state.filterError"
type="error"
v-if="state.filterError != null"
/>
<!-- 包数据表 -->
<PacketTable
:columns="state.columns"
:data="state.packetFrames"
:selectedFrame="state.selectedFrame"
:onSelectedFrame="handleSelectedFrame"
:onScrollBottom="handleScrollBottom"
></PacketTable>
<a-row :gutter="20">
<a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTreeEntry"
:selected="state.selectedTreeEntry"
:tree="state.selectedPacket.tree"
/>
</a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump">
<!-- 报文数据 -->
<a-tabs
v-model:activeKey="state.selectedDataSourceIndex"
:tab-bar-gutter="16"
:tab-bar-style="{ marginBottom: '8px' }"
>
<a-tab-pane
:key="idx"
:tab="v.name"
v-for="(v, idx) in state.selectedPacket.data_sources"
style="overflow: auto"
>
<DissectionDump
:base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected="
idx === state.selectedTreeEntry.idx
? state.selectedTreeEntry
: NO_SELECTION
"
/>
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</a-card>
</PageContainer>
</template>
<style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-info {
flex: 1;
text-align: right;
padding-right: 8px;
}
.summary {
display: flex;
flex-direction: column;
}
.summary-item > span:first-child {
font-weight: 600;
margin-right: 6px;
}
.tree {
font-size: 0.8125rem;
line-height: 1.5rem;
padding-bottom: 0.75rem;
padding-top: 0.75rem;
white-space: nowrap;
overflow-y: auto;
user-select: none;
height: 100%;
}
.tree > ul.tree {
min-height: 15rem;
}
.dump {
padding-bottom: 0.75rem;
padding-top: 0.75rem;
overflow-y: auto;
height: 100%;
}
.dump .ant-tabs-tabpane {
min-height: calc(15rem - 56px);
}
</style>

View File

@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, toRaw, ref } from 'vue'; import { reactive, onMounted, toRaw, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { Form, message, Modal } from 'ant-design-vue/lib'; import { Form, message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider'; import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table'; import { ColumnsType } from 'ant-design-vue/lib/table';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
@@ -23,6 +25,8 @@ import { parseObjHumpToLine } from '@/utils/parse-utils';
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
const { getDict } = useDictStore(); const { getDict } = useDictStore();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const route = useRoute();
/**字典数据 */ /**字典数据 */
let dict: { let dict: {
@@ -94,34 +98,34 @@ let tableState: TabeStateType = reactive({
/**表格字段列 */ /**表格字段列 */
let tableColumns: ColumnsType = [ let tableColumns: ColumnsType = [
{ {
title: t('common.rowId'), title: t('views.ne.common.neType'),
dataIndex: 'id',
align: 'center',
},
{
title: t('views.traceManage.task.neType'),
dataIndex: 'neType', dataIndex: 'neType',
align: 'center', align: 'left',
sorter: { sorter: {
compare: (a, b) => 1, compare: (a, b) => 1,
multiple: 1, multiple: 1,
}, },
}, },
{ {
title: t('views.traceManage.task.neID'), title: t('views.ne.common.neId'),
dataIndex: 'neId', dataIndex: 'neId',
align: 'center', align: 'left',
},
{
title: t('views.traceManage.task.traceId'),
dataIndex: 'traceId',
align: 'left',
}, },
{ {
title: t('views.traceManage.task.trackType'), title: t('views.traceManage.task.trackType'),
dataIndex: 'traceType', dataIndex: 'traceType',
key: 'traceType', key: 'traceType',
align: 'center', align: 'left',
}, },
{ {
title: t('views.traceManage.task.startTime'), title: t('views.traceManage.task.startTime'),
dataIndex: 'startTime', dataIndex: 'startTime',
align: 'center', align: 'left',
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); return parseDateToStr(opt.value);
@@ -131,7 +135,7 @@ let tableColumns: ColumnsType = [
{ {
title: t('views.traceManage.task.endTime'), title: t('views.traceManage.task.endTime'),
dataIndex: 'endTime', dataIndex: 'endTime',
align: 'center', align: 'left',
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); return parseDateToStr(opt.value);
@@ -140,7 +144,7 @@ let tableColumns: ColumnsType = [
{ {
title: t('common.operate'), title: t('common.operate'),
key: 'id', key: 'id',
align: 'center', align: 'left',
}, },
]; ];
@@ -312,6 +316,7 @@ let modalState: ModalStateType = reactive({
id: '', id: '',
neType: '', neType: '',
neId: '', neId: '',
traceId: '',
traceType: '3', traceType: '3',
startTime: undefined, startTime: undefined,
endTime: undefined, endTime: undefined,
@@ -545,6 +550,16 @@ function fnModalCancel() {
modalState.neTypeInterface = []; modalState.neTypeInterface = [];
} }
/**跳转PCAP文件详情页面 */
function fnRecordPCAPView(row: Record<string, any>) {
router.push({
path: `${route.path}${MENU_PATH_INLINE}/analyze`,
query: {
traceId: row.traceId,
},
});
}
onMounted(() => { onMounted(() => {
// 初始字典数据 // 初始字典数据
Promise.allSettled([getDict('trace_type')]).then(resArr => { Promise.allSettled([getDict('trace_type')]).then(resArr => {
@@ -600,15 +615,12 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal"> <a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
<a-form-item <a-form-item :label="t('views.ne.common.neType')" name="neType ">
:label="t('views.traceManage.task.neType')"
name="neType "
>
<a-auto-complete <a-auto-complete
v-model:value="queryParams.neType" v-model:value="queryParams.neType"
:options="neCascaderOptions" :options="neCascaderOptions"
allow-clear allow-clear
:placeholder="t('views.traceManage.task.neTypePlease')" :placeholder="t('views.ne.common.neTypePlease')"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@@ -751,7 +763,7 @@ onMounted(() => {
</template> </template>
<template v-if="column.key === 'id'"> <template v-if="column.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip> <a-tooltip placement="topRight">
<template #title>{{ t('common.editText') }}</template> <template #title>{{ t('common.editText') }}</template>
<a-button <a-button
type="link" type="link"
@@ -760,7 +772,15 @@ onMounted(() => {
<template #icon><FormOutlined /></template> <template #icon><FormOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip placement="topRight">
<template #title>{{
t('views.traceManage.task.pcapView')
}}</template>
<a-button type="link" @click.prevent="fnRecordPCAPView(record)">
<template #icon><ContainerOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.deleteText') }}</template> <template #title>{{ t('common.deleteText') }}</template>
<a-button <a-button
type="link" type="link"