Discover: sidebarFilter layout done

This commit is contained in:
chiayin
2023-04-12 16:44:47 +08:00
parent 98f37249bc
commit daed99f84f
12 changed files with 733 additions and 55 deletions

View File

@@ -0,0 +1,347 @@
<template>
<Sidebar :visible="sidebarFilter" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" class="!w-11/12 !bg-neutral-100">
<template #header>
<ul class="flex space-x-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('filter')" :class="tab === 'filter'? 'text-neutral-900': 'text-neutral-500'">Filter</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('funnel')" :class="tab === 'funnel'? 'text-neutral-900': 'text-neutral-500'">Funnel</li>
</ul>
</template>
<!-- header: filter -->
<div v-if="tab === 'filter'" class="pt-4 bg-neutral-100 flex w-full h-full">
<!-- title: filter silect -->
<div class="space-y-2 mr-4 w-56">
<div>
<p class="h2">Filter Type</p>
<div v-for="(item, index) in selectFilter['Filter Type']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[0]" :inputId="item + index" name="Filter Type" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence'">
<p class="h2">Activity Sequence</p>
<div v-for="(item, index) in selectFilter['Activity Sequence']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[1]" :inputId="item + index" name="Activity Sequence" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[1] === 'Start activity & end activity'">
<p class="h2">Start & End</p>
<div v-for="(item, index) in selectFilter['Start & End']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[2]" :inputId="item + index" name="Start & End" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['Mode']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[3]" :inputId="item + index" name="Mode" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Attributes'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['ModeAtt']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[4]" :inputId="item + index" name="ModeAtt" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div>
<p class="h2">Refine</p>
<div v-for="(item, index) in selectFilter['Refine']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[5]" :inputId="item + index" name="Refinee" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Timeframes'">
<p class="h2">Containment</p>
<div v-for="(item, index) in selectFilter['Containment']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[6]" :inputId="item + index" name="Containment" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
</div>
<!-- title: Activity Select -->
<div class="space-y-2 w-[calc(100%_-_240px)] h-[calc(100%_-_106px)]">
<p class="h2 ml-1">Activity Select</p>
<!-- Filter task Data-->
<ActOccCase v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Have activity(s)'" :tableTitle="'Activity List'" :tableData="filterTaskData" :tableSelect="selectFilterTask" :progressWidth ="progressWidth" @on-row-select="onRowAct"></ActOccCase>
<!-- Filter Start Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'Start'" :tableTitle="'Start activity'" :tableData="filterStartData" :tableSelect="selectFilterStart" :progressWidth ="progressWidth" @on-row-select="onRowStart"></ActOcc>
<!-- Filter End Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'End'" :tableTitle="'End activity'" :tableData="filterEndData" :tableSelect="selectFilterEnd" :progressWidth ="progressWidth" @on-row-select="onRowEnd"></ActOcc>
<!-- Filter Start And End Data -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'Start & End'" class="flex justify-between items-center w-full h-full space-x-4 ">
<ActOcc :tableTitle="'Start activity'" :tableData="filterStartToEndData" :tableSelect="selectFilterStartToEnd" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="startRow"></ActOcc>
<ActOcc :tableTitle="'End activity'" :tableData="filterEndToStartData" :tableSelect="selectFilterEndToStart" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="endRow"></ActOcc>
</div>
<!-- Filter Sequence -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'" class="flex justify-between items-center w-full h-full space-x-4">
<ActAndSeq :filterTaskData="filterTaskData" :progressWidth ="progressWidth" :listSeq="listSeq" @update:listSeq="onUpdateListSeq"></ActAndSeq>
</div>
<!-- Button -->
<div class="float-right space-x-4 px-4 py-2">
<button class="btn btn-sm btn-neutral" @click="reset">Reset</button>
<button class="btn btn-sm btn-neutral">Apply</button>
</div>
</div>
</div>
<!-- header: funnel -->
<div v-if="tab === 'funnel'"></div>
</Sidebar>
</template>
<script>
import ActOccCase from '@/components/Discover/table/actOccCase.vue';
import ActOcc from '@/components/Discover/table/actOcc.vue';
import ActAndSeq from '@/components/Discover/table/actAndSeq.vue';
export default {
props: {
sidebarFilter: {
type: Boolean,
require: true,
},
filterTasks: {
type: Array,
require: true,
},
filterStartToEnd: {
type: Array,
require: true,
},
filterEndToStart: {
type: Array,
require: true,
},
filterTimeframe: {
type: Object,
require: true,
},
filterTrace: {
type: Array,
require: true,
},
},
data() {
return {
selectFilter: {
'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'],
'Activity Sequence':['Have activity(s)', 'Start activity & end activity', 'Sequence'],
'Start & End': ['Start', 'End', 'Start & End'],
'Mode': ['Directly follows', 'Eventually follows'],
'ModeAtt': ['Case', 'Activity'],
'Refine': ['Include', 'Exclude'],
'Containment': ['Contained in', 'Started in', 'End in', 'Activity in', 'Trim'],
},
tab: 'filter',
selectValue: ['Sequence', 'Have activity(s)', 'Start', 'Directly follows', 'Case', 'Include', 'Contained in'],
selectFilterTask: null,
selectFilterStart: null,
selectFilterEnd: null,
selectFilterStartToEnd: null,
selectFilterEndToStart: null,
listSeq: [],
//若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
isStartSelected: null,
isEndSelected: null,
rowData: [],
}
},
components: {
ActOccCase,
ActOcc,
ActAndSeq,
},
computed: {
// All Task
filterTaskData: function() {
let list = [];
this.filterTasks.forEach((task, index) => {
let data = {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
case_value: Number(task.case_ratio * 100),
cases: task.cases.toLocaleString('en-US'),
case_ratio: this.getPercentLabel(task.case_ratio),
};
list.push(data);
});
list.sort((x, y) => y.occurrences - x.occurrences);
return list;
},
// Start and End Task
filterStartData: function() {
return this.setActData(this.filterStartToEnd);
},
filterEndData: function() {
return this.setActData(this.filterEndToStart);
},
filterStartToEndData: function() {
return this.isEndSelected ? this.setStartAndEndData(this.filterEndToStart, this.rowData, 'sources') : this.setActData(this.filterStartToEnd);
},
filterEndToStartData: function() {
return this.isStartSelected ? this.setStartAndEndData(this.filterStartToEnd, this.rowData, 'sinks') : this.setActData(this.filterEndToStart);
}
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? `${val * 100}%` : `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
// 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容
/**
* @param {array} array filterStartToEnd / filterEndToStart
*/
setActData(array) {
let list = [];
array.forEach((task, index) => {
let data = {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
};
list.push(data);
});
return list;
},
/**
* @param {array} select select Have activity(s) rows
*/
onRowAct(select){
this.selectFilterTask = select;
},
/**
* @param {object} e select Start rows
*/
onRowStart(e){
this.selectFilterStart = e.data;
},
/**
* @param {object} e select End rows
*/
onRowEnd(e){
this.selectFilterEnd = e.data;
},
/**
* @param {array} e Update List Seq
*/
onUpdateListSeq(listSeq) {
console.log(this.listSeq);
this.listSeq = listSeq;
},
// 在 Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
/**
* @param {object} e object contains selected row's data
*/
startRow(e){
this.selectFilterStartToEnd = e.data;
if(this.isStartSelected === null || this.isStartSelected === true){
this.isStartSelected = true;
this.isEndSelected = false;
this.rowData = e.data;
}
},
endRow(e) {
this.selectFilterEndToStart = e.data;
if(this.isEndSelected === null || this.isEndSelected === true){
this.isEndSelected = true;
this.isStartSelected = false;
this.rowData = e.data;
}
},
// 重新設定連動的 filterStartToEndData / filterEndToStartData 內容
/**
* @param {array} eventData Start or End List
* @param {object} rowData 所選擇的 row's data
* @param {string} event sinks / sources
*/
setStartAndEndData(eventData, rowData, event){
const filterData = event === 'sinks' ? this.filterEndToStart : this.filterStartToEnd;
const relatedItems = eventData
.find(task => task.label === rowData.label)
?.[event]?.filter(item => filterData.some(ele => ele.label === item))
?.map(item => filterData.find(ele => ele.label === item));
if (!relatedItems) return [];
return relatedItems.map(item => ({
label: item.label,
occ_value: Number(item.occurrence_ratio * 100),
occurrences: Number(item.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(item.occurrence_ratio),
}));
// 以下是優化前的程式碼:
// let list = [];
// eventData.forEach((task) => {
// if(task.label === rowData.label) {
// task[event].forEach(item => {
// const filterData = event === 'sinks' ? this.filterEndToStart : this.filterStartToEnd;
// const element = filterData.find(ele => ele.label === item);
// if (element !== undefined) {
// const data = {
// label: element.label,
// occ_value: Number(element.occurrence_ratio * 100),
// occurrences: Number(element.occurrences).toLocaleString('en-US'),
// occurrence_ratio: this.getPercentLabel(element.occurrence_ratio),
// };
// list.push(data);
// };
// })
// };
// });
// return list;
},
/**
* 清空選項
*/
reset() {
this.selectFilterTask = null;
this.selectFilterStart = null;
this.selectFilterEnd = null;
this.selectFilterStartToEnd = null;
this.selectFilterEndToStart = null;
this.listSeq = [];
this.isStartSelected = null;
this.isEndSelected = null;
// this.rowData = [];
console.log('reset');
}
},
}
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{
appearance: none;
}
</style>

View File

@@ -19,7 +19,7 @@
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @dblclick="switchCaseData(trace.id)">
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id)">
<td class="text-xs p-2">#{{ trace.id }}</td>
<td class="text-xs p-2 w-24">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
@@ -37,7 +37,7 @@
<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 scrollbar overflow-x-auto">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
</div>
</div>

View File

@@ -0,0 +1,86 @@
<template>
<!-- Activity List -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Activity List&nbsp({{ data.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<table class="border-separate border-spacing-x-2 table-auto min-w-full" :class="data.length === 0? 'h-full': null">
<thead class="sticky top-0 left-0 z-10 bg-neutral-10">
<tr>
<th class="text-start text-base font-semibold leading-10 px-2 border-b border-neutral-500">Activity</th>
<th class="text-base font-semibold leading-10 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<Draggable :list="data" group="people" itemKey="name" tag="tbody" animation="300" @end="onEnd">
<template #item="{ element, index }">
<tr>
<td class="px-4 py-2">{{ element.label }}</td>
<td class="px-4 py-2 w-24">
<div class="h-4 min-w-[96px] bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(element.occ_value)"></div>
</div>
</td>
<td class="px-4 py-2 text-right">{{ element.occurrences }}</td>
<td class="px-4 py-2 text-right">{{ element.occurrence_ratio }}</td>
</tr>
</template>
</Draggable>
</table>
</div>
</div>
<!-- Sequence -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative">
<p class="h2 border-b border-500 my-2">Sequence&nbsp({{ listSeq.length }})</p>
<!-- No Data -->
<div v-if="listSequence.length === 0" class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute">
<p class="text-neutral-500">Please drag and drop activity(s) here and sort.</p>
</div>
<!-- Have Data -->
<div class="py-4 m-auto w-full h-[calc(100%_-_56px)]">
<div class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center">
<draggable class="h-full" :list="listSequence" group="people" itemKey="name" animation="300" @end="onEnd">
<template #item="{ element, index }">
<div>
<div class="w-full p-2 border border-primary rounded text-primary">
<span>{{ element.label }}</span>
</div>
<span v-show="index !== listSeq.length - 1" class="pi pi-chevron-down !text-lg inline-block py-2 "></span>
</div>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
filterTaskData: {
type: Array,
required: true,
},
progressWidth: {
type: Function,
required: false,
},
listSeq: {
type: Array,
required: true,
}
},
data() {
return {
listSequence: this.listSeq,
data: this.filterTaskData,
}
},
methods: {
onEnd() {
this.$emit('update:listSeq', this.listSequence);
}
}
}
</script>

View File

@@ -0,0 +1,71 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">{{ tableTitle }}&nbsp({{ tableData.length }})</p>
<!-- Search -->
<!-- <Search></Search> -->
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="tableData" dataKey="label" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto" @row-select="onRowSelect">
<ColumnGroup type="header">
<Row>
<Column selectionMode="single" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occ_value" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="single" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
</div>
</template>
<script>
import Search from '@/components/Search.vue';
export default {
props: {
tableTitle: {
type: String,
required: true,
},
tableData: {
type: Array,
required: true,
},
tableSelect: {
type: [Object, Array],
default: null
},
progressWidth: {
type: Function,
required: false,
}
},
data() {
return {
select: this.tableSelect,
metaKey: true
}
},
components: {
Search,
},
methods: {
onRowSelect(e) {
this.$emit('on-row-select', e)
}
},
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 h-full">
<div class="flex justify-between items-center my-2">
<p class="h2">{{ tableTitle }}&nbsp({{ data.length }})</p>
<!-- Search -->
<!-- <Search></Search> -->
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="data" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll" @row-unselect-all="onRowUnelectAll">
<ColumnGroup type="header">
<Row>
<Column selectionMode="multiple" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occ_value" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
<Column field="case_value" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" header="Cases with Activity" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="O2" bodyClass="!text-right !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.case_value)"></div>
</div>
</template>
</Column>
<Column field="cases" header="Cases with Activity" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="case_ratio" header="C2" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
</div>
</template>
<script>
import Search from '@/components/Search.vue';
export default {
props: {
tableTitle: {
type: String,
require: true,
},
tableData: {
type: Array,
require: true,
},
tableSelect: {
type: Array,
require: true,
},
progressWidth: {
type: Function,
require: false,
}
},
data() {
return {
select: this.tableSelect,
data: this.tableData
}
},
components: {
Search,
},
methods: {
onRowSelect() {
this.$emit('on-row-select', this.select);
},
onRowUnselect() {
this.$emit('on-row-select', this.select);
},
onRowSelectAll(e) {
this.select = e.data;
this.$emit('on-row-select', this.select);
},
onRowUnelectAll(e) {
this.select = null;
this.$emit('on-row-select', this.select)
}
}
}
</script>