Files
lucia-frontend/src/components/Discover/Map/SidebarState.vue
2026-03-06 18:57:58 +08:00

402 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<Sidebar :visible="sidebarState" :closeIcon="'pi pi-angle-right'" :modal="false" position="right" :dismissable="false"
class="!w-[360px]" @hide="hide" @show="show">
<template #header>
<ul class="flex space-x-4 pl-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700"
@click="switchTab('summary')" :class="tab === 'summary'? 'text-neutral-900': ''">Summary</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700"
@click="switchTab('insight')" :class="tab === 'insight'? 'text-neutral-900': ''">Insight</li>
</ul>
</template>
<!-- header: summary -->
<div v-if="tab === 'summary'">
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">{{ i18next.t("Map.FileName") }}</p>
<div class="flex items-center">
<div class="blue-dot w-3 h-3 bg-[#0099FF] rounded-full mr-2"></div><span>{{ currentMapFile }}</span>
</div>
</li>
</ul>
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-[12px]">{{ stats.cases.count.toLocaleString() }} / {{ stats.cases.total.toLocaleString() }}</span>
<ProgressBar :value="valueCases" :showValue="false" class="!h-2 !rounded-full my-1 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-[20px] text-right font-medium basis-28">{{ getPercentLabel(stats.cases.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-[12px]">{{ stats.traces.count.toLocaleString() }} / {{ stats.traces.total.toLocaleString() }}</span>
<ProgressBar :value="valueTraces" :showValue="false" class="!h-2 !rounded-full my-1 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-[20px] text-right font-medium basis-28">{{ getPercentLabel(stats.traces.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-[12px]">{{ stats.task_instances.count.toLocaleString() }} / {{ stats.task_instances.total.toLocaleString() }}</span>
<ProgressBar :value="valueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-1 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-[20px] text-right font-medium basis-28">{{ getPercentLabel(stats.task_instances.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-[12px]">{{ stats.tasks.count.toLocaleString() }} / {{ stats.tasks.total.toLocaleString() }}</span>
<ProgressBar :value="valueTasks" :showValue="false" class="!h-2 !rounded-full my-1 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-[20px] text-right font-medium basis-28">{{ getPercentLabel(stats.tasks.ratio) }}</span>
</div>
</li>
</ul>
<!-- Log Timeframe -->
<div class="pt-1 pb-4 border-b border-neutral-300">
<p class="h2">Log Timeframe</p>
<p class="text-sm flex items-center">
<div class="blue-dot w-3 h-3 bg-[#0099FF] rounded-full mr-2 flex"></div>
<span class="pr-1 flex">{{ moment(stats.started_at) }}</span>
~
<span class="pl-1 flex">{{ moment(stats.completed_at) }}</span>
</p>
</div>
<!-- Case Duration -->
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<table class="text-sm caseDurationTable">
<caption class="hidden">Case Duration</caption>
<th class="hidden"></th>
<tbody>
<tr>
<td>
<Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>
</td>
<td class="text-[#0099FF] flex w-20 justify-end">
{{ timeLabel(stats.case_duration.min)[1] + ' ' + timeLabel(stats.case_duration.min)[2] }}
</td>
</tr>
<tr>
<td>
<Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>
</td>
<td class="text-[#0099FF] flex w-20 justify-end">
{{ timeLabel(stats.case_duration.average)[1] + ' ' + timeLabel(stats.case_duration.average)[2] }}
</td>
</tr>
<tr>
<td>
<Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>
</td>
<td class="text-[#0099FF] flex w-20 justify-end">
{{ timeLabel(stats.case_duration.median)[1] + ' ' + timeLabel(stats.case_duration.median)[2] }}
</td>
</tr>
<tr>
<td>
<Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>
</td>
<td class="text-[#0099FF] flex w-20 justify-end">
{{ timeLabel(stats.case_duration.max)[1] + ' ' + timeLabel(stats.case_duration.max)[2] }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- header: insight -->
<div v-if="tab === 'insight'">
<div class="border-b-2 border-neutral-300 mb-4">
<p class="h2">Most Frequent</p>
<ul class="list-disc ml-6 mb-2 text-sm">
<li class="leading-5">Activity:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_tasks" :key="key">{{ value }}</span>
</li>
<li class="leading-5">Inbound connections:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_in" :key="key">{{ value }}
</span>
</li>
<li class="leading-5">Outbound connections:&nbsp;
<span class="text-[#0099FF] break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key) in
insights.most_freq_out" :key="key">{{ value }}
</span>
</li>
</ul>
<p class="h2">Most Time-Consuming</p>
<ul class="list-disc ml-6 mb-4 text-sm">
<li class="w-full leading-5">Activity:&nbsp;
<span class="text-primary break-words bg-[#F1F5F9] px-2 rounded mx-1" v-for="(value, key)
in insights.most_time_tasks" :key="key">{{ value }}
</span>
</li>
<li class="w-full leading-5 mt-2">Connection:&nbsp;
<span class="text-primary break-words"
v-for="(item, key) in insights.most_time_edges" :key="key">
<span v-for="(value, index) in item" :key="index">
<span class="connection-text bg-[#F1F5F9] px-2 rounded">{{ value }}</span>
<span v-if="index !== item.length - 1">&nbsp;
<span class="material-symbols-outlined !text-lg align-sub ">arrow_forward</span>
&nbsp;</span>
</span>
<span v-if="key !== insights.most_time_edges.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
</ul>
</div>
<div>
<ul class="trace-buttons text-neutral-500 grid grid-cols-2 gap-2 text-center text-sm font-medium mb-2">
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="onActiveTraceClick(0)" :class="activeTrace === 0? 'text-primary border-primary':''">Self-Loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="onActiveTraceClick(1)" :class="activeTrace === 1? 'text-primary border-primary':''">Short-Loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="onActiveTraceClick(2)" :class="activeTrace === 2? 'text-primary border-primary':''">Shortest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="onActiveTraceClick(3)" :class="activeTrace === 3? 'text-primary border-primary':''">Longest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="onActiveTraceClick(4)" :class="activeTrace === 4? 'text-primary border-primary':''">Most Frequent Trace</li>
</ul>
<div class="reset-trace-button underline text-[#4E5969] text-[14px] flex justify-end cursor-pointer
font-semibold" @click="onResetTraceBtnClick">
{{ i18next.t("Map.Reset") }}
</div>
<div>
<TabView ref="tabview2" v-model:activeIndex="activeTrace">
<TabPanel header="Self-loop" contentClass="text-sm">
<p v-if="insights.self_loops.length === 0">No data</p>
<ul v-else class="ml-6 space-y-1">
<li v-for="(value, key) in insights.self_loops" :key="key">
<span>{{ value }}</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Short-loop" contentClass="text-sm">
<p v-if="insights.short_loops.length === 0">No data</p>
<ul v-else class="ml-6 space-y-1">
<li class="break-words" v-for="(item, key) in insights.short_loops" :key="key">
<span v-for="(value, index) in item" :key="index">
{{ value }}
<span v-if="index !== item.length - 1">&nbsp;
<span class="material-symbols-outlined !text-lg align-sub">sync_alt</span>
&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<!-- 從shortest_traces開始迭代 -->
<TabPanel v-for="([field, label], i) in fieldNamesAndLabelNames" :key="i" :header="label"
contentClass="text-sm">
<p v-if="insights[field].length === 0" class="bg-neutral-100 p-2 rounded">No data</p>
<ul v-else class="ml-1 space-y-1">
<li v-for="(item, key2) in insights[field]" :key="key2"
class="mb-2 flex bg-neutral-100 p-2 rounded">
<div class="flex left-col mr-1">
<input type="radio" name="customRadio" :value="key2" v-model="clickedPathListIndex"
class="hidden peer" @click="onPathOptionClick(key2)"
/>
<!-- 若為BPMN檢視模式則不允許點亮路徑 -->
<span v-if="!isBPMNOn" @click="onPathOptionClick(key2)"
:class="[
'w-[18px] h-[18px] rounded-full border-2 inline-flex items-center justify-center cursor-pointer bg-[#FFFFFF]',
clickedPathListIndex === key2
? 'border-[#0099FF]'
: 'border-[#CBD5E1]'
]"
>
<div
:class="[
'w-[9px] h-[9px] rounded-full transition-opacity cursor-pointer',
clickedPathListIndex === key2
? 'bg-[#0099FF]'
: 'opacity-0'
]"
></div>
</span>
</div>
<div class="right-col">
<span v-for="(value, index) in item" :key="index">
{{ value }}
<span v-if="index !== item.length - 1">
&nbsp;
<span class="material-symbols-outlined !text-lg align-sub">arrow_forward</span>
&nbsp;
</span>
</span>
</div>
</li>
</ul>
</TabPanel>
</TabView>
</div>
</div>
</div>
</Sidebar>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/SidebarState
* Summary statistics sidebar for the Map view
* displaying cases, traces, activities, timeframe,
* and case duration.
*/
import { computed, ref } from 'vue';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
import i18next from '@/i18n/i18n';
import { INSIGHTS_FIELDS_AND_LABELS } from '@/constants/constants';
// 刪除第一個和第二個元素
const fieldNamesAndLabelNames = [...INSIGHTS_FIELDS_AND_LABELS].slice(2);
const props = defineProps({
sidebarState: {
type: Boolean,
require: false,
},
stats: {
type: Object,
required: false,
},
insights: {
type: Object,
required: false,
}
});
const pageAdmin = usePageAdminStore();
const mapPathStore = useMapPathStore();
const activeTrace = ref(0);
const currentMapFile = computed(() => pageAdmin.currentMapFile);
const clickedPathListIndex = ref(0);
const isBPMNOn = computed(() => mapPathStore.isBPMNOn);
const tab = ref('summary');
const valueCases = ref(0);
const valueTraces = ref(0);
const valueTaskInstances = ref(0);
const valueTasks = ref(0);
/**
* Handles click on an active trace to highlight it.
* @param {number} clickedActiveTraceIndex - The clicked trace index.
*/
function onActiveTraceClick(clickedActiveTraceIndex) {
mapPathStore.clearAllHighlight();
activeTrace.value = clickedActiveTraceIndex;
mapPathStore.highlightClickedPath(clickedActiveTraceIndex, clickedPathListIndex.value);
}
/**
* Handles click on a path option to highlight it.
* @param {number} clickedPath - The clicked path index.
*/
function onPathOptionClick(clickedPath) {
clickedPathListIndex.value = clickedPath;
mapPathStore.highlightClickedPath(activeTrace.value, clickedPath);
}
/** Resets the trace highlight to default. */
function onResetTraceBtnClick() {
if(isBPMNOn.value) {
return;
}
clickedPathListIndex.value = undefined;
}
/**
* @param {string} newTab Summary or Insight
*/
function switchTab(newTab) {
tab.value = newTab;
}
/**
* @param {number} time use timeLabel.js
*/
function timeLabel(time){ // sonar-qube prevent super-linear runtime due to backtracking; change * to ?
//
const label = getTimeLabel(time).replace(/\s+/g, ' '); // 將所有連續空白字符壓縮為一個空白
const result = label.match(/^(\d+)\s?([a-zA-Z]+)$/); // add ^ and $ to meet sonar-qube need
return result;
}
/**
* @param {number} time use moment
*/
function moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm');
}
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
/**
* Behavior when show
*/
function show(){
valueCases.value = props.stats.cases.ratio * 100;
valueTraces.value = props.stats.traces.ratio * 100;
valueTaskInstances.value = props.stats.task_instances.ratio * 100;
valueTasks.value = props.stats.tasks.ratio * 100;
}
/**
* Behavior when hidden
*/
function hide(){
valueCases.value = 0;
valueTraces.value = 0;
valueTaskInstances.value = 0;
valueTasks.value = 0;
}
</script>
<style scoped>
@reference "../../../assets/tailwind.css";
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
:deep(.p-tabview-nav-container) {
@apply hidden
}
:deep(.p-tabview-panels) {
@apply p-2 rounded
}
:deep(.p-tabview-panel) {
@apply animate-fadein
}
.caseDurationTable td {
@apply scroll-pb-12
}
.caseDurationTable td:nth-child(2) {
@apply text-right
}
.caseDurationTable td:last-child {
@apply pl-2
}
</style>