Files
lucia-frontend/src/components/Discover/Map/SidebarTraces.vue
2023-11-06 14:49:25 +08:00

282 lines
9.3 KiB
Vue

<template>
<Sidebar :visible="sidebarTraces" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" class="!w-11/12" @show="show()">
<template #header>
<p class="h1">Traces</p>
</template>
<div class="pt-4 h-full flex items-center justify-start">
<!-- Trace List -->
<section class="w-80 h-full pr-4 border-r border-neutral-300">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
<span class="material-symbols-outlined text-sm align-[-10%] mr-2">info</span>Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<table class="border-separate border-spacing-x-2 text-sm">
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id, trace.base_count)">
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2">{{ trace.ratio }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Trace item Table -->
<section class="pl-4 h-full w-[calc(100%_-_320px)]">
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-52 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_264px)] infiniteTable" @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
<div v-for="(col, index) in columnData" :key="index">
<Column :field="col.field" :header="col.header"></Column>
</div>
</DataTable>
</div>
</section>
</div>
</Sidebar>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
props: ['sidebarTraces', 'cases'],
setup() {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
return {allMapDataStore, infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases, isLoading }
},
data() {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: null,
infinitMaxItems: false,
infiniteData: [],
}
},
computed: {
traceTotal: function() {
return this.traces.length;
},
traceList: function() {
let sum = this.traces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
let result = this.traces.map(trace => {
return {
id: trace.id,
value: this.progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: this.getPercentLabel(trace.count / sum),
};
})
return result;
},
caseData: function() {
const data = JSON.parse(JSON.stringify(this.infiniteData)); // 深拷貝原始 cases 的內容
data.forEach(item => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // 建立新的 key-value pair
});
delete item.attributes; // 刪除原本的 attributes 屬性
})
return data;
},
columnData: function() {
const data = JSON.parse(JSON.stringify(this.cases)); // 深拷貝原始 cases 的內容
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0]?.attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
];
}
return result
},
},
watch: {
infinite404: function(newValue) {
if(newValue === 404) this.infinitMaxItems = true;
},
traceId: {
handler(newValue) {
this.showTraceId = newValue;
},
immediate: true
},
showTraceId: function(newValue, oldValue) {
let isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
},
infiniteFirstCases: function(newValue){
if(this.infiniteFirstCases) this.infiniteData = JSON.parse(JSON.stringify(newValue));
}
},
methods: {
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id
*/
async switchCaseData(id, count) {
if(count >= 1000) this.isLoading = true;
this.infinit404 = null;
this.infinitMaxItems = false;
this.showTraceId = id;
this.infiniteStart = 0;
this.$emit('switch-Trace-Id', {id: this.showTraceId, count: count});
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.traceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.traceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
let graphId = this.$refs.cyTrace;
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
/**
* create map
*/
async show() {
this.isLoading = await true; // createCy 執行完關閉
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 traces 第一筆 id
this.showTraceId = await this.traces[0]?.id;
this.infiniteStart = await 0;
this.setNodesData();
this.setEdgesData();
this.createCy();
this.isLoading = false;
},
/**
* 無限滾動: 監聽 scroll 有沒有滾到底部
* @param {element} event
*/
handleScroll(event) {
if(this.infinitMaxItems || this.cases.length < 20) return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) this.fetchData();
},
/**
* 無限滾動: 滾到底後,要載入數據
*/
async fetchData() {
try {
this.infiniteStart += 20;
await this.allMapDataStore.getTraceDetail();
this.infiniteData = [...this.infiniteData, ...this.cases];
} catch(error) {
console.error('Failed to load data:', error);
}
}
},
}
</script>
<style scoped>
/* 進度條顏色 */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
/* Table set */
:deep(.p-datatable-thead th) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0
}
</style>