Files
lucia-frontend/src/views/Discover/Map/index.vue

368 lines
13 KiB
Vue

<template>
<!-- Sidebar: Switch data type -->
<div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10" :class="sidebarLeftValue? 'bg-neutral-50':''">
<ul class="space-y-4 flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarView = !sidebarView" :class="{'border-primary': sidebarView}">
<span class="material-symbols-outlined text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
track_changes
</span>
</li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': sidebarFilter}">
<span class="material-symbols-outlined text-2xl hover:text-primary p-1.5" :class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
tornado
</span>
</li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces" :class="{'border-primary': sidebarTraces}">
<span class="material-symbols-outlined text-2xl hover:text-primary p-1.5" :class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
rebase
</span>
</li>
</ul>
<!-- <ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary">
<span class="material-symbols-outlined text-2xl text-neutral-500 hover:text-primary p-1.5">
highlight
</span>
</li>
</ul> -->
</div>
<!-- Cytoscape Map -->
<div class="min-w-full h-screen-main bg-neutral-50 -z-40">
<div id="cy" class="min-w-full h-screen-main"></div>
</div>
<!-- Sidebar: State -->
<div class="bg-transparent py-4 w-14 h-screen-main z-10 bottom-0 right-0 absolute">
<ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState" :class="{'border-primary': sidebarState}" id="iconState">
<span class="material-symbols-outlined text-2xl text-neutral-500 hover:text-primary p-1.5" :class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info
</span>
</li>
</ul>
</div>
<!-- Sidebar Model -->
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType" ></SidebarView>
<SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState>
<SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesView"></SidebarTraces>
<SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd" :filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidevarFilterRef"></SidebarFilter>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
import cytoscapeMap from '@/module/cytoscapeMap.js';
import SidebarView from '@/components/Discover/Map/SidebarView.vue';
import SidebarState from '@/components/Discover/Map/SidebarState.vue';
import SidebarTraces from '@/components/Discover/Map/SidebarTraces.vue';
import SidebarFilter from '@/components/Discover/Map/SidebarFilter.vue';
export default {
setup() {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, temporaryData, isRuleData, ruleData, logId, baseLogId, createFilterId, cases } = storeToRefs(allMapDataStore);
return { isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces, baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData, ruleData, allMapDataStore, cases }
},
components: {
SidebarView,
SidebarState,
SidebarTraces,
SidebarFilter,
},
data() {
return {
processMapData: {
startId: 0,
endId: 1,
nodes: [],
edges: [],
},
bpmnData: {
startId: 0,
endId: 1,
nodes: [],
edges: [],
},
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
dataLayerType: 'freq', // freq | duration
dataLayerOption: 'total',
rank: 'LR', // 直向 TB | 橫向 LR
traceId: 1,
sidebarView: false, // SideBar: Visualization Setting
sidebarState: false, // SideBar: Summary & Insight
sidebarTraces: false, // SideBar: Traces
sidebarFilter: false, // SideBar: Filter
infiniteFirstCases: null,
}
},
computed:{
sidebarLeftValue: function() {
let result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true;
return result;
}
},
watch: {
sidebarView: function(newValue) {
if(newValue) {
this.sidebarFilter = false;
this.sidebarTraces = false;
}
},
sidebarFilter: function(newValue) {
if(newValue) {
this.sidebarView = false;
this.sidebarState = false;
this.sidebarTraces = false;
this.sidebarState = false;
}
},
sidebarTraces: function(newValue) {
if(newValue) {
this.sidebarView = false;
this.sidebarState = false;
this.sidebarFilter = false;
this.sidebarState = false;
}
},
sidebarState: function(newValue) {
if(newValue) {
this.sidebarFilter = false;
this.sidebarTraces = false;
}
},
},
methods: {
/**
* switch map type
* @param {string} type processMap | bpmn
*/
switchMapType(type) {
this.mapType = type;
this.createCy(type);
},
/**
* switch curve style
* @param {string} style 直角 unbundled-bezier | taxi
*/
switchCurveStyles(style) {
this.curveStyle = style;
this.createCy(this.mapType);
},
/**
* switch rank
* @param {string} rank 直向 TB | 橫向 LR
*/
switchRank(rank) {
this.rank = rank;
this.createCy(this.mapType);
},
/**
* switch Data Layoer Type or Option.
* @param {string} e
* @param {string} type freq | duration
*/
switchDataLayerType(type, option){
this.dataLayerType = type;
this.dataLayerOption = option;
this.createCy(this.mapType);
},
/**
* switch trace id and data
* @param {string} id
*/
async switchTraceId(e) {
if(e.count >= 1000) this.isLoading = true;
this.traceId = e.id;
await this.allMapDataStore.getTraceDetail();
this.$refs.tracesView.createCy();
this.isLoading = false;
},
/**
* 將 element nodes 資料彙整
* @param {object} type processMapData | bpmnData
*/
setNodesData(mapData) {
let mapType = this.mapType;
const logFreq = {
"total": "",
"rel_freq": "",
"average": "",
"median": "",
"max": "",
"min": "",
"cases": ""
};
const logDuration = {
"total": "",
"rel_duration": "",
"average": "",
"median": "",
"max": "",
"min": "",
};
// BPMN 才有 gateway 類別
const gateway = {
parallel: "+",
exclusive: "x",
inclusive: "o",
};
// 避免每次渲染都重複累加
mapData.nodes = [];
// 將 api call 回來的資料帶進 node
this[mapType].vertices.forEach(node => {
switch (node.type) {
// add type of 'bpmn gateway' node
case 'gateway':
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:gateway[node.gateway_type],
height:60,
width:60,
backgroundColor:'#FFF',
bordercolor:'#003366',
shape:"diamond",
freq:logFreq,
duration:logDuration,
}
})
break;
// add type of 'event' node
case 'event':
if(node.event_type === 'start') mapData.startId = node.id;
else if(node.event_type === 'end') mapData.endId = node.id;
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:node.event_type,
height:60,
width:60,
backgroundColor:'#FFCCCC',
bordercolor:'#003366',
shape:"ellipse",
freq:logFreq,
duration:logDuration,
}
});
break;
// add type of 'activity' node
default:
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:node.label,
height:80,
width:100,
backgroundColor:'#FFCCCC',
bordercolor:'#003366',
shape:"round-rectangle",
freq:node.freq,
duration:node.duration,
}
})
break;
}
});
},
/**
* 將 element edges 資料彙整
* @param {object} type processMapData | bpmnData
*/
setEdgesData(mapData) {
let mapType = this.mapType;
//add event duration is empty
const logDuration = {
"total": "",
"rel_duration": "",
"average": "",
"median": "",
"max": "",
"min": "",
"cases": ""
};
mapData.edges = [];
this[mapType].edges.map(edge => {
mapData.edges.push({
data: {
source:edge.tail,
target:edge.head,
freq:edge.freq,
duration:edge.duration === null ? logDuration : edge.duration,
style:'dotted',
lineWidth:1,
},
});
});
},
/**
* create cytoscape's map
* @param {string} type this.mapType processMap | bpmn
*/
createCy(type) {
let graphId = document.getElementById('cy');
let mapData = type === 'processMap'? this.processMapData: this.bpmnData;
if(this[type].vertices.length !== 0){
this.setNodesData(mapData);
this.setEdgesData(mapData);
cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
};
},
},
async created() {
// 先 loading 再執行以下程式
this.isLoading = true;
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁
if(this.$route.params.type === 'log'){
this.logId = this.$route.params.fileId;
}else if(this.$route.params.type === 'filter') {
this.createFilterId = this.$route.params.fileId;
// 取得 logID 和上次儲存的 Funnel
await this.allMapDataStore.fetchFunnel(this.createFilterId);
this.isRuleData = await Array.from(this.temporaryData);
this.ruleData = await this.isRuleData.map(e => this.$refs.sidevarFilterRef.setRule(e));
}
// 取得 logId 後才 call api
await this.allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllTrace();
// log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。
this.traceId = await this.traces[0]?.id;
this.baseTraceId = await this.baseTraces[0]?.id;
this.createCy(this.mapType);
await this.allMapDataStore.getFilterParams();
await this.allMapDataStore.getTraceDetail();
// 執行完後才取消 loading
this.isLoading = false;
// 存檔 Modal 打開時,側邊欄要關閉
this.$emitter.on('saveModal', boolean => {
this.sidebarView = boolean;
this.sidebarFilter = boolean;
this.sidebarTraces = boolean;
this.sidebarState = boolean;
});
this.$emitter.on('leaveFilter', boolean => {
this.sidebarView = boolean;
this.sidebarFilter = boolean;
this.sidebarTraces = boolean;
this.sidebarState = boolean;
});
},
}
</script>