Apply repository-wide ESLint auto-fix formatting pass

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
2026-03-08 12:11:57 +08:00
parent 7c48faaa3d
commit 847904c49b
172 changed files with 13629 additions and 9154 deletions

View File

@@ -4,8 +4,12 @@
// imacat.yang@dsp.im (imacat), 2023/9/23
/** @module auth Authentication token refresh utilities. */
import axios from 'axios';
import { getCookie, setCookie, setCookieWithoutExpiration } from '@/utils/cookieUtil.js';
import axios from "axios";
import {
getCookie,
setCookie,
setCookieWithoutExpiration,
} from "@/utils/cookieUtil.js";
/**
* Refreshes the access token using the stored refresh token cookie.
@@ -18,27 +22,29 @@ import { getCookie, setCookie, setCookieWithoutExpiration } from '@/utils/cookie
* @throws {Error} If the refresh request fails.
*/
export async function refreshTokenAndGetNew() {
const api = '/api/oauth/token';
const api = "/api/oauth/token";
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
"Content-Type": "application/x-www-form-urlencoded",
},
};
const data = {
grant_type: 'refresh_token',
refresh_token: getCookie('luciaRefreshToken'),
grant_type: "refresh_token",
refresh_token: getCookie("luciaRefreshToken"),
};
const response = await axios.post(api, data, config);
const newAccessToken = response.data.access_token;
const newRefreshToken = response.data.refresh_token;
setCookieWithoutExpiration('luciaToken', newAccessToken);
setCookieWithoutExpiration("luciaToken", newAccessToken);
// Expire in ~6 months
const expiredMs = new Date();
expiredMs.setMonth(expiredMs.getMonth() + 6);
const days = Math.ceil((expiredMs.getTime() - Date.now()) / (24 * 60 * 60 * 1000));
setCookie('luciaRefreshToken', newRefreshToken, days);
const days = Math.ceil(
(expiredMs.getTime() - Date.now()) / (24 * 60 * 60 * 1000),
);
setCookie("luciaRefreshToken", newRefreshToken, days);
return newAccessToken;
}

View File

@@ -8,15 +8,15 @@
* 401 token refresh with request queuing.
*/
import axios from 'axios';
import { getCookie, deleteCookie } from '@/utils/cookieUtil.js';
import axios from "axios";
import { getCookie, deleteCookie } from "@/utils/cookieUtil.js";
/** Axios instance configured with auth interceptors. */
const apiClient = axios.create();
// Request interceptor: automatically attach Authorization header
apiClient.interceptors.request.use((config) => {
const token = getCookie('luciaToken');
const token = getCookie("luciaToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
@@ -54,7 +54,7 @@ apiClient.interceptors.response.use(
if (
error.response?.status !== 401 ||
originalRequest._retried ||
originalRequest.url === '/api/oauth/token'
originalRequest.url === "/api/oauth/token"
) {
return Promise.reject(error);
}
@@ -76,7 +76,7 @@ apiClient.interceptors.response.use(
try {
// Dynamic import to avoid circular dependency with login store
const { refreshTokenAndGetNew } = await import('@/api/auth.js');
const { refreshTokenAndGetNew } = await import("@/api/auth.js");
const newToken = await refreshTokenAndGetNew();
isRefreshing = false;
onRefreshSuccess(newToken);
@@ -87,11 +87,11 @@ apiClient.interceptors.response.use(
onRefreshFailure(refreshError);
// Refresh failed: clear auth and redirect to login
deleteCookie('luciaToken');
window.location.href = '/login';
deleteCookie("luciaToken");
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
},
);
export default apiClient;

View File

@@ -1,32 +1,51 @@
<template>
<div id="account_menu" v-if="isAcctMenuOpen" class="absolute top-0 w-[232px] bg-white right-[0px] rounded shadow-lg bg-[#ffffff]">
<div id="greeting" class="w-full border-b border-[#CBD5E1]">
<span class="m-4 h-[48px]">
{{ i18next.t("AcctMgmt.hi") }}{{ userData.name }}
</span>
</div>
<ul class="w-full min-h-10">
<!-- Not using a loop here because SVGs won't display if src is a variable -->
<li v-if="isAdmin" id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnAcctMgmtClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-crown.svg" alt="accountManagement"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.acctMgmt")}}</span>
</li>
<li id="btn_mang_ur_acct"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onBtnMyAccountClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-head-black.svg" alt="head-black"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.mangUrAcct")}}</span>
</li>
<li id="btn_logout_in_menu"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer
items-center" @click="onLogoutBtnClick">
<span class="w-[24px] h-[24px] flex"><img src="@/assets/icon-logout.svg" alt="logout"></span>
<span class="flex ml-[8px]">{{i18next.t("AcctMgmt.Logout")}}</span>
</li>
</ul>
<div
id="account_menu"
v-if="isAcctMenuOpen"
class="absolute top-0 w-[232px] bg-white right-[0px] rounded shadow-lg bg-[#ffffff]"
>
<div id="greeting" class="w-full border-b border-[#CBD5E1]">
<span class="m-4 h-[48px]">
{{ i18next.t("AcctMgmt.hi") }}{{ userData.name }}
</span>
</div>
<ul class="w-full min-h-10">
<!-- Not using a loop here because SVGs won't display if src is a variable -->
<li
v-if="isAdmin"
id="btn_acct_mgmt"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onBtnAcctMgmtClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-crown.svg" alt="accountManagement"
/></span>
<span class="flex ml-[8px]">{{ i18next.t("AcctMgmt.acctMgmt") }}</span>
</li>
<li
id="btn_mang_ur_acct"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onBtnMyAccountClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-head-black.svg" alt="head-black"
/></span>
<span class="flex ml-[8px]">{{
i18next.t("AcctMgmt.mangUrAcct")
}}</span>
</li>
<li
id="btn_logout_in_menu"
class="w-full h-[40px] flex py-2 px-4 hover:text-[#000000] hover:bg-[#F1F5F9] cursor-pointer items-center"
@click="onLogoutBtnClick"
>
<span class="w-[24px] h-[24px] flex"
><img src="@/assets/icon-logout.svg" alt="logout"
/></span>
<span class="flex ml-[8px]">{{ i18next.t("AcctMgmt.Logout") }}</span>
</li>
</ul>
</div>
</template>
<script setup>
@@ -40,16 +59,16 @@
* with links to account management, my account, and logout.
*/
import { computed, onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import i18next from '@/i18n/i18n';
import { useRouter, useRoute } from 'vue-router';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import emitter from '@/utils/emitter';
import { computed, onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import i18next from "@/i18n/i18n";
import { useRouter, useRoute } from "vue-router";
import { useLoginStore } from "@/stores/login";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
import emitter from "@/utils/emitter";
const router = useRouter();
const route = useRoute();
@@ -60,12 +79,15 @@ const acctMgmtStore = useAcctMgmtStore();
const { logOut } = loginStore;
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } = storeToRefs(conformanceStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } =
storeToRefs(conformanceStore);
const { userData } = storeToRefs(loginStore);
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
const loginUserData = ref(null);
const currentViewingUserDetail = computed(() => acctMgmtStore.currentViewingUser.detail);
const currentViewingUserDetail = computed(
() => acctMgmtStore.currentViewingUser.detail,
);
const isAdmin = ref(false);
/** Fetches user data and determines if the current user is an admin. */
@@ -81,16 +103,21 @@ const onBtnMyAccountClick = async () => {
acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push('/my-account');
await router.push("/my-account");
};
/** Registers a click listener to close the menu when clicking outside. */
const clickOtherPlacesThenCloseMenu = () => {
const acctMgmtButton = document.getElementById('acct_mgmt_button');
const acctMgmtMenu = document.getElementById('account_menu');
const acctMgmtButton = document.getElementById("acct_mgmt_button");
const acctMgmtMenu = document.getElementById("account_menu");
document.addEventListener('click', (event) => {
if (acctMgmtMenu && acctMgmtButton && !acctMgmtMenu.contains(event.target) && !acctMgmtButton.contains(event.target)) {
document.addEventListener("click", (event) => {
if (
acctMgmtMenu &&
acctMgmtButton &&
!acctMgmtMenu.contains(event.target) &&
!acctMgmtButton.contains(event.target)
) {
acctMgmtStore.closeAcctMenu();
}
});
@@ -98,19 +125,29 @@ const clickOtherPlacesThenCloseMenu = () => {
/** Navigates to the Account Admin page. */
const onBtnAcctMgmtClick = () => {
router.push({name: 'AcctAdmin'});
router.push({ name: "AcctAdmin" });
acctMgmtStore.closeAcctMenu();
};
/** Handles logout with unsaved-changes confirmation for Map and Conformance pages. */
const onLogoutBtnClick = () => {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
if (
(route.name === "Map" || route.name === "CheckMap") &&
tempFilterId.value
) {
// Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
emitter.emit("leaveFilter", false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut);
} else if (
(route.name === "Conformance" || route.name === "CheckConformance") &&
(conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)
) {
leaveConformance(
false,
conformanceStore.addConformanceCreateCheckId,
false,
logOut,
);
} else {
logOut();
}

View File

@@ -1,12 +1,22 @@
<template>
<div id="search_bar_container" class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full
justify-between">
<input id="input_search" class="w-full outline-0" :placeholder="i18next.t('AcctMgmt.Search')"
v-model="inputQuery" @keypress="handleKeyPressOfSearch"
/>
<img src="@/assets/icon-search.svg" class="w-[17px] h-[17px] flex cursor-pointer" @click="onSearchClick"
alt="search"/>
</div>
<div
id="search_bar_container"
class="flex w-[280px] h-8 px-4 items-center border-[#64748B] border-[1px] rounded-full justify-between"
>
<input
id="input_search"
class="w-full outline-0"
:placeholder="i18next.t('AcctMgmt.Search')"
v-model="inputQuery"
@keypress="handleKeyPressOfSearch"
/>
<img
src="@/assets/icon-search.svg"
class="w-[17px] h-[17px] flex cursor-pointer"
@click="onSearchClick"
alt="search"
/>
</div>
</template>
<script setup>
@@ -20,10 +30,10 @@
* filtering accounts, emits search query on click or Enter key.
*/
import { ref } from 'vue';
import i18next from '@/i18n/i18n.js';
import { ref } from "vue";
import i18next from "@/i18n/i18n.js";
const emit = defineEmits(['on-search-account-button-click']);
const emit = defineEmits(["on-search-account-button-click"]);
const inputQuery = ref("");
@@ -32,8 +42,8 @@ const inputQuery = ref("");
* @param {Event} event - The click event.
*/
const onSearchClick = (event) => {
event.preventDefault();
emit('on-search-account-button-click', inputQuery.value);
event.preventDefault();
emit("on-search-account-button-click", inputQuery.value);
};
/**
@@ -41,8 +51,8 @@ const onSearchClick = (event) => {
* @param {KeyboardEvent} event - The keypress event.
*/
const handleKeyPressOfSearch = (event) => {
if (event.key === 'Enter') {
emit('on-search-account-button-click', inputQuery.value);
}
if (event.key === "Enter") {
emit("on-search-account-button-click", inputQuery.value);
}
};
</script>

View File

@@ -1,18 +1,18 @@
<template>
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
:class="{
'badge-activated': isActivated,
'badge-deactivated': !isActivated,
'bg-[#0099FF]': isActivated,
'bg-[#C9CDD4]': !isActivated,
}"
>
{{ displayText }}
</div>
</template>
<div
class="status-badge rounded-full max-w-[95px] w-fit px-3 inline-flex items-center text-[#ffffff] text-[14px] mr-2"
:class="{
'badge-activated': isActivated,
'badge-deactivated': !isActivated,
'bg-[#0099FF]': isActivated,
'bg-[#C9CDD4]': !isActivated,
}"
>
{{ displayText }}
</div>
</template>
<script setup>
<script setup>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
@@ -23,16 +23,16 @@
* an activated/deactivated state with colored background.
*/
defineProps({
isActivated: {
type: Boolean,
required: true,
default: true,
},
displayText: {
type: String,
required: true,
default: "Status",
}
});
</script>
defineProps({
isActivated: {
type: Boolean,
required: true,
default: true,
},
displayText: {
type: String,
required: true,
default: "Status",
},
});
</script>

View File

@@ -1,18 +1,16 @@
<template>
<button class="button-component w-[80px] h-[32px] rounded-full
flex text-[#666666] border-[1px] border-[#666666]
justify-center items-center bg-[#FFFFFF]
hover:text-[#0099FF] hover:border-[#0099FF]
focus:text-[#0099FF] focus:border-[#0099FF]
cursor-pointer"
:class="{
'ring': isPressed,
'ring-[#0099FF]' : isPressed,
'ring-opacity-30': isPressed,
}"
@mousedown="onMousedown" @mouseup="onMouseup">
{{ buttonText }}
</button>
<button
class="button-component w-[80px] h-[32px] rounded-full flex text-[#666666] border-[1px] border-[#666666] justify-center items-center bg-[#FFFFFF] hover:text-[#0099FF] hover:border-[#0099FF] focus:text-[#0099FF] focus:border-[#0099FF] cursor-pointer"
:class="{
ring: isPressed,
'ring-[#0099FF]': isPressed,
'ring-opacity-30': isPressed,
}"
@mousedown="onMousedown"
@mouseup="onMouseup"
>
{{ buttonText }}
</button>
</template>
<script setup>
@@ -26,22 +24,22 @@
* press-state ring effect.
*/
import { ref } from 'vue';
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
isPressed.value = false;
};
</script>

View File

@@ -1,19 +1,18 @@
<!-- The filled version of the button has a solid background -->
<template>
<button class="button-filled-component w-[80px] h-[32px] rounded-full
flex text-[#FFFFFF]
justify-center items-center bg-[#0099FF]
hover:text-[#FFFFFF] hover:bg-[#0080D5]
cursor-pointer"
:class="{
'ring': isPressed,
'ring-[#0099FF]' : isPressed,
'ring-opacity-30': isPressed,
'bg-[#0099FF]': isPressed,
}"
@mousedown="onMousedown" @mouseup="onMouseup">
{{ buttonText }}
</button>
<button
class="button-filled-component w-[80px] h-[32px] rounded-full flex text-[#FFFFFF] justify-center items-center bg-[#0099FF] hover:text-[#FFFFFF] hover:bg-[#0080D5] cursor-pointer"
:class="{
ring: isPressed,
'ring-[#0099FF]': isPressed,
'ring-opacity-30': isPressed,
'bg-[#0099FF]': isPressed,
}"
@mousedown="onMousedown"
@mouseup="onMouseup"
>
{{ buttonText }}
</button>
</template>
<script setup>
@@ -27,22 +26,22 @@
* solid background and press-state ring effect.
*/
import { ref } from 'vue';
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
isPressed.value = false;
};
</script>

View File

@@ -1,5 +1,14 @@
<template>
<Sidebar :visible="sidebarState" :closeIcon="'pi pi-angle-right'" :modal="false" position="right" :dismissable="false" class="!w-[440px]" @hide="hide" @show="show">
<Sidebar
:visible="sidebarState"
:closeIcon="'pi pi-angle-right'"
:modal="false"
position="right"
:dismissable="false"
class="!w-[440px]"
@hide="hide"
@show="show"
>
<template #header>
<p class="pl-2 text-base font-bold text-neutral-900">Summary</p>
</template>
@@ -9,7 +18,12 @@
<section class="w-[204px] box-border pr-4">
<div class="mb-4">
<p class="h2">File Name</p>
<p class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis" :title="primaryStatData.name">{{ primaryStatData.name }}</p>
<p
class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis"
:title="primaryStatData.name"
>
{{ primaryStatData.name }}
</p>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
@@ -17,40 +31,80 @@
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.cases.count }} / {{ primaryStatData.cases.total }}</span>
<ProgressBar :value="primaryValueCases" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.cases.count }} /
{{ primaryStatData.cases.total }}</span
>
<ProgressBar
:value="primaryValueCases"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.cases.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.cases.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.traces.count }} / {{ primaryStatData.traces.total }}</span>
<ProgressBar :value="primaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.traces.count }} /
{{ primaryStatData.traces.total }}</span
>
<ProgressBar
:value="primaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.traces.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.traces.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.task_instances.count }} / {{ primaryStatData.task_instances.total }}</span>
<ProgressBar :value="primaryValueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.task_instances.count }} /
{{ primaryStatData.task_instances.total }}</span
>
<ProgressBar
:value="primaryValueTaskInstances"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.task_instances.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.task_instances.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ primaryStatData.tasks.count }} / {{ primaryStatData.tasks.total }}</span>
<ProgressBar :value="primaryValueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"></ProgressBar>
<span class="block text-sm"
>{{ primaryStatData.tasks.count }} /
{{ primaryStatData.tasks.total }}</span
>
<ProgressBar
:value="primaryValueTasks"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-primary"
></ProgressBar>
</div>
<span class="block text-primary text-2xl text-right font-medium basis-28">{{ primaryStatData.tasks.ratio }}%</span>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
@@ -67,10 +121,34 @@
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1 text-sm">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.min }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.average }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.median }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ primaryStatData.case_duration.max }}</li>
<li>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.min }}
</li>
<li>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.average }}
</li>
<li>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.median }}
</li>
<li>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ primaryStatData.case_duration.max }}
</li>
</ul>
</div>
</section>
@@ -78,7 +156,12 @@
<section class="w-[204px] box-border pl-4">
<div class="mb-4">
<p class="h2">File Name</p>
<p class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis" :title="secondaryStatData.name">{{ secondaryStatData.name }}</p>
<p
class="text-sm leading-normal whitespace-nowrap break-keep overflow-hidden text-ellipsis"
:title="secondaryStatData.name"
>
{{ secondaryStatData.name }}
</p>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
@@ -86,40 +169,80 @@
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.cases.count }} / {{ secondaryStatData.cases.total }}</span>
<ProgressBar :value="secondaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.cases.count }} /
{{ secondaryStatData.cases.total }}</span
>
<ProgressBar
:value="secondaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.cases.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.cases.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.traces.count }} / {{ secondaryStatData.traces.total }}</span>
<ProgressBar :value="secondaryValueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.traces.count }} /
{{ secondaryStatData.traces.total }}</span
>
<ProgressBar
:value="secondaryValueTraces"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.traces.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.traces.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.task_instances.count }} / {{ secondaryStatData.task_instances.total }}</span>
<ProgressBar :value="secondaryValueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.task_instances.count }} /
{{ secondaryStatData.task_instances.total }}</span
>
<ProgressBar
:value="secondaryValueTaskInstances"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.task_instances.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.task_instances.ratio }}%</span
>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-4">
<span class="block text-sm">{{ secondaryStatData.tasks.count }} / {{ secondaryStatData.tasks.total }}</span>
<ProgressBar :value="secondaryValueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"></ProgressBar>
<span class="block text-sm"
>{{ secondaryStatData.tasks.count }} /
{{ secondaryStatData.tasks.total }}</span
>
<ProgressBar
:value="secondaryValueTasks"
:showValue="false"
class="!h-2 !rounded-full my-2 !bg-neutral-300 progressbar-secondary"
></ProgressBar>
</div>
<span class="block text-secondary text-2xl text-right font-medium basis-28">{{ secondaryStatData.tasks.ratio }}%</span>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
@@ -136,10 +259,34 @@
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1 text-sm">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.min }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.average }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.median }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ secondaryStatData.case_duration.max }}</li>
<li>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.min }}
</li>
<li>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.average }}
</li>
<li>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.median }}
</li>
<li>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"
></Tag
>{{ secondaryStatData.case_duration.max }}
</li>
</ul>
</div>
</section>
@@ -158,11 +305,11 @@
* traces, activities, timeframes, durations) of two files.
*/
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useCompareStore } from '@/stores/compare';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useCompareStore } from "@/stores/compare";
import { getTimeLabel } from "@/module/timeLabel.js";
import getMoment from "moment";
const props = defineProps({
sidebarState: {
@@ -191,7 +338,7 @@ const secondaryStatData = ref(null);
* @returns {number} The percentage value.
*/
const getPercentLabel = (val) => {
if((val * 100).toFixed(1) >= 100) return 100;
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
};
@@ -205,34 +352,34 @@ const getStatData = (data, fileName) => {
return {
name: fileName,
cases: {
count: data.cases.count.toLocaleString('en-US'),
total: data.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.cases.ratio)
count: data.cases.count.toLocaleString("en-US"),
total: data.cases.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.cases.ratio),
},
traces: {
count: data.traces.count.toLocaleString('en-US'),
total: data.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.traces.ratio)
count: data.traces.count.toLocaleString("en-US"),
total: data.traces.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.traces.ratio),
},
task_instances: {
count: data.task_instances.count.toLocaleString('en-US'),
total: data.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.task_instances.ratio)
count: data.task_instances.count.toLocaleString("en-US"),
total: data.task_instances.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.task_instances.ratio),
},
tasks: {
count: data.tasks.count.toLocaleString('en-US'),
total: data.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(data.tasks.ratio)
count: data.tasks.count.toLocaleString("en-US"),
total: data.tasks.total.toLocaleString("en-US"),
ratio: getPercentLabel(data.tasks.ratio),
},
started_at: getMoment(data.started_at).format('YYYY.MM.DD HH:mm'),
completed_at: getMoment(data.completed_at).format('YYYY.MM.DD HH:mm'),
started_at: getMoment(data.started_at).format("YYYY.MM.DD HH:mm"),
completed_at: getMoment(data.completed_at).format("YYYY.MM.DD HH:mm"),
case_duration: {
min: getTimeLabel(data.case_duration.min, 2),
max: getTimeLabel(data.case_duration.max, 2),
average: getTimeLabel(data.case_duration.average, 2),
median: getTimeLabel(data.case_duration.median, 2),
}
}
},
};
};
/** Populates progress bar values when the sidebar is shown. */
@@ -243,7 +390,8 @@ const show = () => {
primaryValueTasks.value = primaryStatData.value.tasks.ratio;
secondaryValueCases.value = secondaryStatData.value.cases.ratio;
secondaryValueTraces.value = secondaryStatData.value.traces.ratio;
secondaryValueTaskInstances.value = secondaryStatData.value.task_instances.ratio;
secondaryValueTaskInstances.value =
secondaryStatData.value.task_instances.ratio;
secondaryValueTasks.value = secondaryStatData.value.tasks.ratio;
};
@@ -266,10 +414,13 @@ onMounted(async () => {
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const primaryData = await compareStore.getStateData(primaryType, primaryId);
const secondaryData = await compareStore.getStateData(secondaryType, secondaryId);
const secondaryData = await compareStore.getStateData(
secondaryType,
secondaryId,
);
const primaryFileName = await compareStore.getFileName(primaryId)
const secondaryFileName = await compareStore.getFileName(secondaryId)
const primaryFileName = await compareStore.getFileName(primaryId);
const secondaryFileName = await compareStore.getFileName(secondaryId);
primaryStatData.value = await getStatData(primaryData, primaryFileName);
secondaryStatData.value = await getStatData(secondaryData, secondaryFileName);
});
@@ -279,9 +430,9 @@ onMounted(async () => {
background-color: var(--bg-color);
}
.progressbar-primary {
--bg-color: #0099FF;
--bg-color: #0099ff;
}
.progressbar-secondary {
--bg-color: #FFAA44;
--bg-color: #ffaa44;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,30 @@
<template>
<div class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">Activity list</p>
<div class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]" id="cyp-conformance-list-checkbox">
<div class="flex items-center w-[166px]" v-for="(act, index) in sortData" :key="index" :title="act">
<Checkbox v-model="actList" :inputId="index.toString()" name="actList" :value="act" @change="actListData"/>
<label :for="index" class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</label>
<div
class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]"
id="cyp-conformance-list-checkbox"
>
<div
class="flex items-center w-[166px]"
v-for="(act, index) in sortData"
:key="index"
:title="act"
>
<Checkbox
v-model="actList"
:inputId="index.toString()"
name="actList"
:value="act"
@change="actListData"
/>
<label
:for="index"
class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ act }}</label
>
</div>
</div>
</div>
@@ -20,30 +40,37 @@
* Checkbox-based activity list for conformance checking input.
*/
import { ref, watch } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'select']);
const props = defineProps(["data", "select"]);
const sortData = ref([]);
const actList = ref(props.select);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(
() => props.data,
(newValue) => {
sortData.value = sortNumEngZhtw(newValue);
},
{ immediate: true },
);
watch(() => props.select, (newValue) => {
actList.value = newValue;
});
watch(
() => props.select,
(newValue) => {
actList.value = newValue;
},
);
/** Emits the selected activities list via the event bus. */
function actListData() {
emitter.emit('actListData', actList.value);
emitter.emit("actListData", actList.value);
}
// created
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
actList.value = data;
});
</script>

View File

@@ -1,10 +1,29 @@
<template>
<div class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">{{ title }}</p>
<div class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]">
<div class="flex items-center w-[166px]" v-for="(act, index) in sortData" :key="index" :title="act">
<RadioButton v-model="selectedRadio" :inputId="index + act" :name="select" :value="act" @change="actRadioData" />
<label :for="index + act" class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</label>
<div
class="flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar h-[calc(100%_-_48px)]"
>
<div
class="flex items-center w-[166px]"
v-for="(act, index) in sortData"
:key="index"
:title="act"
>
<RadioButton
v-model="selectedRadio"
:inputId="index + act"
:name="select"
:value="act"
@change="actRadioData"
/>
<label
:for="index + act"
class="ml-2 p-2 whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ act }}</label
>
</div>
</div>
</div>
@@ -22,13 +41,20 @@
* start/end activity input.
*/
import { ref, computed, watch } from 'vue';
import { ref, computed, watch } from "vue";
import { useConformanceInputStore } from "@/stores/conformanceInput";
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['title', 'select', 'data', 'category', 'task', 'isSubmit']);
const emit = defineEmits(['selected-task']);
const props = defineProps([
"title",
"select",
"data",
"category",
"task",
"isSubmit",
]);
const emit = defineEmits(["selected-task"]);
const conformanceInputStore = useConformanceInputStore();
@@ -36,13 +62,20 @@ const sortData = ref([]);
const localSelect = ref(null);
const selectedRadio = ref(null);
watch(() => props.data, (newValue) => {
sortData.value = sortNumEngZhtw(newValue);
}, { immediate: true });
watch(
() => props.data,
(newValue) => {
sortData.value = sortNumEngZhtw(newValue);
},
{ immediate: true },
);
watch(() => props.task, (newValue) => {
selectedRadio.value = newValue;
});
watch(
() => props.task,
(newValue) => {
selectedRadio.value = newValue;
},
);
const inputActivityRadioData = computed(() => ({
category: props.category,
@@ -52,22 +85,27 @@ const inputActivityRadioData = computed(() => ({
/** Emits the selected activity via event bus and updates the store. */
function actRadioData() {
localSelect.value = null;
emitter.emit('actRadioData', inputActivityRadioData.value);
emit('selected-task', selectedRadio.value);
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task);
emitter.emit("actRadioData", inputActivityRadioData.value);
emit("selected-task", selectedRadio.value);
conformanceInputStore.setActivityRadioStartEndData(
inputActivityRadioData.value.task,
);
}
/** Sets the global activity radio data state in the conformance input store. */
function setGlobalActivityRadioDataState() {
//this.title: value might be "From" or "To"
conformanceInputStore.setActivityRadioStartEndData(inputActivityRadioData.value.task, props.title);
conformanceInputStore.setActivityRadioStartEndData(
inputActivityRadioData.value.task,
props.title,
);
}
// created
sortNumEngZhtw(sortData.value);
localSelect.value = props.isSubmit ? props.select : null;
selectedRadio.value = localSelect.value;
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
selectedRadio.value = data;
});
setGlobalActivityRadioDataState();

View File

@@ -1,38 +1,99 @@
<template>
<div class="h-full w-full flex justify-between items-center">
<!-- Activity List -->
<div class="h-full w-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2">
<div
class="h-full w-full bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2"
>
<p class="h2 pl-2 border-b mb-3">Activity list</p>
<div class="h-[calc(100%_-_56px)]">
<Draggable :list="datadata" :group="{name: 'activity', pull: 'clone' }" itemKey="name" animation="300" :fallbackTolerance="5" :forceFallback="true" :ghostClass="'ghostSelected'" :dragClass="'dragSelected'" :sort="false" @end="onEnd" class="h-full flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar">
<Draggable
:list="datadata"
:group="{ name: 'activity', pull: 'clone' }"
itemKey="name"
animation="300"
:fallbackTolerance="5"
:forceFallback="true"
:ghostClass="'ghostSelected'"
:dragClass="'dragSelected'"
:sort="false"
@end="onEnd"
class="h-full flex flex-wrap justify-start content-start gap-4 px-2 overflow-y-auto scrollbar"
>
<template #item="{ element, index }">
<div :class="listSequence.includes(element) ? 'border-primary text-primary' : ''" class="flex items-center w-[166px] border rounded p-2 bg-neutral-10 cursor-pointer hover:bg-primary/20" @dblclick="moveActItem(index, element)" :title="element">
<span class="whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ element }}</span>
<div
:class="
listSequence.includes(element)
? 'border-primary text-primary'
: ''
"
class="flex items-center w-[166px] border rounded p-2 bg-neutral-10 cursor-pointer hover:bg-primary/20"
@dblclick="moveActItem(index, element)"
:title="element"
>
<span
class="whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>{{ element }}</span
>
</div>
</template>
</Draggable>
</div>
</div>
<!-- sequence -->
<div class="w-full h-full relative bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2 text-sm">
<div
class="w-full h-full relative bg-neutral-10 border border-neutral-300 rounded-xl ml-4 p-4 space-y-2 text-sm"
>
<p class="h2 border-b border-500 mb-3">Sequence</p>
<!-- No Data -->
<div v-if="listSequence && 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 at least two activities here and sort.</p>
<div
v-if="listSequence && 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 at least two activities here and sort.
</p>
</div>
<!-- Have Data -->
<div class="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" :group="{name: 'activity'}" :list="listSequence" itemKey="name" animation="300" :forceFallback="true" :dragClass="'dragSelected'" :fallbackTolerance="5" @start="onStart" @end="onEnd" :component-data="getComponentData()" :ghostClass="'!opacity-0'">
<div class="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"
:group="{ name: 'activity' }"
:list="listSequence"
itemKey="name"
animation="300"
:forceFallback="true"
:dragClass="'dragSelected'"
:fallbackTolerance="5"
@start="onStart"
@end="onEnd"
:component-data="getComponentData()"
:ghostClass="'!opacity-0'"
>
<template #item="{ element, index }">
<div :title="element">
<div class="flex justify-center items-center">
<div class="w-full p-2 border rounded bg-neutral-10 cursor-pointer hover:bg-primary/20" @dblclick="moveSeqItem(index, element)">
<div
class="w-full p-2 border rounded bg-neutral-10 cursor-pointer hover:bg-primary/20"
@dblclick="moveSeqItem(index, element)"
>
<span>{{ element }}</span>
</div>
<span class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger" @click.stop.native="moveSeqItem(index, element)">close</span>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop.native="moveSeqItem(index, element)"
>close</span
>
</div>
<span v-show="index !== listSequence.length - 1 && index !== lastItemIndex - 1" class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"></span>
<span
v-show="
index !== listSequence.length - 1 &&
index !== lastItemIndex - 1
"
class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"
></span>
</div>
</template>
</draggable>
@@ -54,11 +115,11 @@
* conformance rule configuration.
*/
import { ref, computed } from 'vue';
import { sortNumEngZhtw } from '@/module/sortNumEngZhtw.js';
import emitter from '@/utils/emitter';
import { ref, computed } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'listSeq', 'isSubmit', 'category']);
const props = defineProps(["data", "listSeq", "isSubmit", "category"]);
const listSequence = ref([]);
const lastItemIndex = ref(null);
@@ -67,7 +128,7 @@ const isSelect = ref(true);
const datadata = computed(() => {
// Sort the Activity List
let newData;
if(props.data !== null) {
if (props.data !== null) {
newData = JSON.parse(JSON.stringify(props.data));
sortNumEngZhtw(newData);
}
@@ -96,7 +157,7 @@ function moveSeqItem(index, element) {
* get listSequence
*/
function getComponentData() {
emitter.emit('getListSequence', {
emitter.emit("getListSequence", {
category: props.category,
task: listSequence.value,
});
@@ -107,13 +168,13 @@ function getComponentData() {
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
originalElement.style.display = 'none';
originalElement.style.display = "none";
// When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
if (evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
@@ -122,12 +183,12 @@ function onStart(evt) {
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
originalElement.style.display = '';
originalElement.style.display = "";
// Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
lastChild.style.display = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
@@ -136,16 +197,16 @@ function onEnd(evt) {
// created
const newlist = JSON.parse(JSON.stringify(props.listSeq));
listSequence.value = props.isSubmit ? newlist : [];
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
listSequence.value = [];
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@apply bg-primary/20
@apply bg-primary/20;
}
.dragSelected {
@apply !opacity-100
@apply !opacity-100;
}
</style>

View File

@@ -1,50 +1,117 @@
<template>
<section class="space-y-2 text-sm">
<section class="space-y-2 text-sm">
<!-- Rule Type -->
<div id="cyp-conformance-type-radio">
<p class="h2">Rule Type</p>
<div v-for="rule in ruleType" :key="rule.id" class="ml-4 mb-2">
<RadioButton v-model="selectedRuleType" :inputId="rule.id + rule.name" name="ruleType" :value="rule.name" @change="changeRadio"/>
<RadioButton
v-model="selectedRuleType"
:inputId="rule.id + rule.name"
name="ruleType"
:value="rule.name"
@change="changeRadio"
/>
<label :for="rule.id + rule.name" class="ml-2">{{ rule.name }}</label>
</div>
</div>
<!-- Activity Sequence (2 item) -->
<div v-show="selectedRuleType === 'Activity sequence'" id="cyp-conformance-sequence-radio">
<div
v-show="selectedRuleType === 'Activity sequence'"
id="cyp-conformance-sequence-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in activitySequence" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActivitySequence" :inputId="act.id + act.name" name="activitySequence" :value="act.name" @change="changeRadioSeq"/>
<RadioButton
v-model="selectedActivitySequence"
:inputId="act.id + act.name"
name="activitySequence"
:value="act.name"
@change="changeRadioSeq"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
<!-- Mode -->
<div v-show="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'" id="cyp-conformance-Mode-radio">
<div
v-show="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence'
"
id="cyp-conformance-Mode-radio"
>
<p class="h2">Mode</p>
<div v-for="mode in mode" :key="mode.id" class="ml-4 mb-2">
<RadioButton v-model="selectedMode" :inputId="mode.id + mode.name" name="mode" :value="mode.name" />
<RadioButton
v-model="selectedMode"
:inputId="mode.id + mode.name"
name="mode"
:value="mode.name"
/>
<label :for="mode.id + mode.name" class="ml-2">{{ mode.name }}</label>
</div>
</div>
<!-- Process Scope -->
<div v-show="selectedRuleType === 'Processing time' || selectedRuleType === 'Waiting time'" id="cyp-conformance-procss-radio">
<div
v-show="
selectedRuleType === 'Processing time' ||
selectedRuleType === 'Waiting time'
"
id="cyp-conformance-procss-radio"
>
<p class="h2">Process Scope</p>
<div v-for="pro in processScope" :key="pro.id" class="ml-4 mb-2">
<RadioButton v-model="selectedProcessScope" :inputId="pro.id + pro.name" name="processScope" :value="pro.name" @change="changeRadioProcessScope"/>
<RadioButton
v-model="selectedProcessScope"
:inputId="pro.id + pro.name"
name="processScope"
:value="pro.name"
@change="changeRadioProcessScope"
/>
<label :for="pro.id + pro.name" class="ml-2">{{ pro.name }}</label>
</div>
</div>
<!-- Activity Sequence (4 item) -->
<div v-show="(selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end') || (selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end') || selectedRuleType === 'Cycle time'" id="cyp-conformance-actseq-radio">
<div
v-show="
(selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end') ||
(selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end') ||
selectedRuleType === 'Cycle time'
"
id="cyp-conformance-actseq-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in actSeqMore" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActSeqMore" :inputId="act.id + act.name" name="activitySequenceMore" :value="act.name" @change="changeRadioActSeqMore"/>
<RadioButton
v-model="selectedActSeqMore"
:inputId="act.id + act.name"
name="activitySequenceMore"
:value="act.name"
@change="changeRadioActSeqMore"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
<!-- Activity Sequence (3 item) -->
<div v-show="(selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial') || (selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial')" id="cyp-conformance-actseqfromto-radio">
<div
v-show="
(selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial') ||
(selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial')
"
id="cyp-conformance-actseqfromto-radio"
>
<p class="h2">Activity Sequence</p>
<div v-for="act in actSeqFromTo" :key="act.id" class="ml-4 mb-2">
<RadioButton v-model="selectedActSeqFromTo" :inputId="act.id + act.name" name="activitySequenceFromTo" :value="act.name" @change="changeRadioActSeqFromTo"/>
<RadioButton
v-model="selectedActSeqFromTo"
:inputId="act.id + act.name"
name="activitySequenceFromTo"
:value="act.name"
@change="changeRadioActSeqFromTo"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
@@ -62,70 +129,77 @@
* sequence, mode, and process scope selection.
*/
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo } = storeToRefs(conformanceStore);
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
} = storeToRefs(conformanceStore);
const ruleType = [
{id: 1, name: 'Have activity'},
{id: 2, name: 'Activity sequence'},
{id: 3, name: 'Activity duration'},
{id: 4, name: 'Processing time'},
{id: 5, name: 'Waiting time'},
{id: 6, name: 'Cycle time'},
{ id: 1, name: "Have activity" },
{ id: 2, name: "Activity sequence" },
{ id: 3, name: "Activity duration" },
{ id: 4, name: "Processing time" },
{ id: 5, name: "Waiting time" },
{ id: 6, name: "Cycle time" },
];
const activitySequence = [
{id: 1, name: 'Start & End'},
{id: 2, name: 'Sequence'},
{ id: 1, name: "Start & End" },
{ id: 2, name: "Sequence" },
];
const mode = [
{id: 1, name: 'Directly follows'},
{id: 2, name: 'Eventually follows'},
{id: 3, name: 'Short loop(s)'},
{id: 4, name: 'Self loop(s)'},
{ id: 1, name: "Directly follows" },
{ id: 2, name: "Eventually follows" },
{ id: 3, name: "Short loop(s)" },
{ id: 4, name: "Self loop(s)" },
];
const processScope = [
{id: 1, name: 'End to end'},
{id: 2, name: 'Partial'},
{ id: 1, name: "End to end" },
{ id: 2, name: "Partial" },
];
const actSeqMore = [
{id: 1, name: 'All'},
{id: 2, name: 'Start'},
{id: 3, name: 'End'},
{id: 4, name: 'Start & End'},
{ id: 1, name: "All" },
{ id: 2, name: "Start" },
{ id: 3, name: "End" },
{ id: 4, name: "Start & End" },
];
const actSeqFromTo = [
{id: 1, name: 'From'},
{id: 2, name: 'To'},
{id: 3, name: 'From & To'},
{ id: 1, name: "From" },
{ id: 2, name: "To" },
{ id: 3, name: "From & To" },
];
/** Resets dependent selections when the rule type radio changes. */
function changeRadio() {
selectedActivitySequence.value = 'Start & End';
selectedMode.value = 'Directly follows';
selectedProcessScope.value = 'End to end';
selectedActSeqMore.value = 'All';
selectedActSeqFromTo.value = 'From';
emitter.emit('isRadioChange', true); // Clear data when switching radio buttons
selectedActivitySequence.value = "Start & End";
selectedMode.value = "Directly follows";
selectedProcessScope.value = "End to end";
selectedActSeqMore.value = "All";
selectedActSeqFromTo.value = "From";
emitter.emit("isRadioChange", true); // Clear data when switching radio buttons
}
/** Emits event when the activity sequence radio changes. */
function changeRadioSeq() {
emitter.emit('isRadioSeqChange',true);
emitter.emit("isRadioSeqChange", true);
}
/** Emits event when the process scope radio changes. */
function changeRadioProcessScope() {
emitter.emit('isRadioProcessScopeChange', true);
emitter.emit("isRadioProcessScopeChange", true);
}
/** Emits event when the extended activity sequence radio changes. */
function changeRadioActSeqMore() {
emitter.emit('isRadioActSeqMoreChange', true);
emitter.emit("isRadioActSeqMoreChange", true);
}
/** Emits event when the from/to activity sequence radio changes. */
function changeRadioActSeqFromTo() {
emitter.emit('isRadioActSeqFromToChange', true);
emitter.emit("isRadioActSeqFromToChange", true);
}
</script>

View File

@@ -1,31 +1,183 @@
<template>
<div class="px-4 text-sm">
<!-- Have activity -->
<ResultCheck v-if="selectedRuleType === 'Have activity'" :data="state.containstTasksData" :select="isSubmitTask"></ResultCheck>
<ResultCheck
v-if="selectedRuleType === 'Have activity'"
:data="state.containstTasksData"
:select="isSubmitTask"
></ResultCheck>
<!-- Activity sequence -->
<ResultDot v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'" :timeResultData="selectCfmSeqSE" :select="isSubmitStartAndEnd"></ResultDot>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Directly follows'" :data="state.selectCfmSeqDirectly" :select="isSubmitCfmSeqDirectly"></ResultArrow>
<ResultArrow v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence' && selectedMode === 'Eventually follows'" :data="state.selectCfmSeqEventually" :select="isSubmitCfmSeqEventually"></ResultArrow>
<ResultDot
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Start & End'
"
:timeResultData="selectCfmSeqSE"
:select="isSubmitStartAndEnd"
></ResultDot>
<ResultArrow
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Directly follows'
"
:data="state.selectCfmSeqDirectly"
:select="isSubmitCfmSeqDirectly"
></ResultArrow>
<ResultArrow
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Eventually follows'
"
:data="state.selectCfmSeqEventually"
:select="isSubmitCfmSeqEventually"
></ResultArrow>
<!-- Activity duration -->
<ResultCheck v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'" :data="state.durationData" :select="isSubmitDurationData"></ResultCheck>
<ResultCheck
v-if="selectedRuleType === 'Activity duration'"
:title="'Activities include'"
:data="state.durationData"
:select="isSubmitDurationData"
></ResultCheck>
<!-- Processing time -->
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmPtEteStart" :select="isSubmitCfmPtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmPtEteEnd" :select="isSubmitCfmPtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmPtEteSE" :select="isSubmitCfmPtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmPtPStart" :select="isSubmitCfmPtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmPtPEnd" :select="isSubmitCfmPtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmPtPSE" :select="isSubmitCfmPtPSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmPtEteStart"
:select="isSubmitCfmPtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmPtEteEnd"
:select="isSubmitCfmPtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmPtEteSE"
:select="isSubmitCfmPtEteSE"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:timeResultData="state.selectCfmPtPStart"
:select="isSubmitCfmPtPStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:timeResultData="state.selectCfmPtPEnd"
:select="isSubmitCfmPtPEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:timeResultData="selectCfmPtPSE"
:select="isSubmitCfmPtPSE"
></ResultDot>
<!-- Waiting time -->
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmWtEteStart" :select="isSubmitCfmWtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmWtEteEnd" :select="isSubmitCfmWtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmWtEteSE" :select="isSubmitCfmWtEteSE"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From'" :timeResultData="state.selectCfmWtPStart" :select="isSubmitCfmWtPStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'To'" :timeResultData="state.selectCfmWtPEnd" :select="isSubmitCfmWtPEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial' && selectedActSeqFromTo === 'From & To'" :timeResultData="selectCfmWtPSE" :select="isSubmitCfmWtPSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmWtEteStart"
:select="isSubmitCfmWtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmWtEteEnd"
:select="isSubmitCfmWtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmWtEteSE"
:select="isSubmitCfmWtEteSE"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:timeResultData="state.selectCfmWtPStart"
:select="isSubmitCfmWtPStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:timeResultData="state.selectCfmWtPEnd"
:select="isSubmitCfmWtPEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:timeResultData="selectCfmWtPSE"
:select="isSubmitCfmWtPSE"
></ResultDot>
<!-- Cycle time -->
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start'" :timeResultData="state.selectCfmCtEteStart" :select="isSubmitCfmCtEteStart"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'End'" :timeResultData="state.selectCfmCtEteEnd" :select="isSubmitCfmCtEteEnd"></ResultDot>
<ResultDot v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'" :timeResultData="selectCfmCtEteSE" :select="isSubmitCfmCtEteSE"></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:timeResultData="state.selectCfmCtEteStart"
:select="isSubmitCfmCtEteStart"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:timeResultData="state.selectCfmCtEteEnd"
:select="isSubmitCfmCtEteEnd"
></ResultDot>
<ResultDot
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:timeResultData="selectCfmCtEteSE"
:select="isSubmitCfmCtEteSE"
></ResultDot>
</div>
</template>
<script setup>
@@ -41,18 +193,49 @@
* scrollable display of check results.
*/
import { reactive, computed, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ResultCheck from '@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue';
import ResultArrow from '@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue';
import ResultDot from '@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue';
import { reactive, computed, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import ResultCheck from "@/components/Discover/Conformance/ConformanceSidebar/ResultCheck.vue";
import ResultArrow from "@/components/Discover/Conformance/ConformanceSidebar/ResultArrow.vue";
import ResultDot from "@/components/Discover/Conformance/ConformanceSidebar/ResultDot.vue";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore, selectedActSeqFromTo, isStartSelected, isEndSelected } = storeToRefs(conformanceStore);
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
isStartSelected,
isEndSelected,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually', 'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE', 'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart', 'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd', 'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE']);
const props = defineProps([
"isSubmit",
"isSubmitTask",
"isSubmitStartAndEnd",
"isSubmitCfmSeqDirectly",
"isSubmitCfmSeqEventually",
"isSubmitDurationData",
"isSubmitCfmPtEteStart",
"isSubmitCfmPtEteEnd",
"isSubmitCfmPtEteSE",
"isSubmitCfmPtPStart",
"isSubmitCfmPtPEnd",
"isSubmitCfmPtPSE",
"isSubmitCfmWtEteStart",
"isSubmitCfmWtEteEnd",
"isSubmitCfmWtEteSE",
"isSubmitCfmWtPStart",
"isSubmitCfmWtPEnd",
"isSubmitCfmWtPSE",
"isSubmitCfmCtEteStart",
"isSubmitCfmCtEteEnd",
"isSubmitCfmCtEteSE",
]);
const state = reactive({
containstTasksData: null,
@@ -87,10 +270,10 @@ const state = reactive({
const selectCfmSeqSE = computed(() => {
const data = [];
if(state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if(state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
if (state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if (state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -98,10 +281,10 @@ const selectCfmSeqSE = computed(() => {
const selectCfmPtEteSE = computed(() => {
const data = [];
if(state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if(state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
if (state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if (state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -109,10 +292,10 @@ const selectCfmPtEteSE = computed(() => {
const selectCfmPtPSE = computed(() => {
const data = [];
if(state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if(state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
if (state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if (state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -120,10 +303,10 @@ const selectCfmPtPSE = computed(() => {
const selectCfmWtEteSE = computed(() => {
const data = [];
if(state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if(state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
if (state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if (state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -131,10 +314,10 @@ const selectCfmWtEteSE = computed(() => {
const selectCfmWtPSE = computed(() => {
const data = [];
if(state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if(state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
if (state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if (state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
data.sort((a, b) => {
const order = { 'From': 1, 'To': 2};
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -142,10 +325,10 @@ const selectCfmWtPSE = computed(() => {
const selectCfmCtEteSE = computed(() => {
const data = [];
if(state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if(state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
if (state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if (state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
data.sort((a, b) => {
const order = { 'Start': 1, 'End': 2};
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
@@ -186,35 +369,35 @@ function reset() {
}
// created() logic
emitter.on('actListData', (data) => {
emitter.on("actListData", (data) => {
state.containstTasksData = data;
});
emitter.on('actRadioData', (newData) => {
emitter.on("actRadioData", (newData) => {
const data = JSON.parse(JSON.stringify(newData)); // Deep copy the original cases data
const categoryMapping = {
'cfmSeqStart': ['Start', 'selectCfmSeqStart', 'selectCfmSeqEnd'],
'cfmSeqEnd': ['End', 'selectCfmSeqEnd', 'selectCfmSeqStart'],
'cfmPtEteStart': ['Start', 'selectCfmPtEteStart'],
'cfmPtEteEnd': ['End', 'selectCfmPtEteEnd'],
'cfmPtEteSEStart': ['Start', 'selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd'],
'cfmPtEteSEEnd': ['End', 'selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart'],
'cfmPtPStart': ['From', 'selectCfmPtPStart'],
'cfmPtPEnd': ['To', 'selectCfmPtPEnd'],
'cfmPtPSEStart': ['From', 'selectCfmPtPSEStart', 'selectCfmPtPSEEnd'],
'cfmPtPSEEnd': ['To', 'selectCfmPtPSEEnd', 'selectCfmPtPSEStart'],
'cfmWtEteStart': ['Start', 'selectCfmWtEteStart'],
'cfmWtEteEnd': ['End', 'selectCfmWtEteEnd'],
'cfmWtEteSEStart': ['Start', 'selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd'],
'cfmWtEteSEEnd': ['End', 'selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart'],
'cfmWtPStart': ['From', 'selectCfmWtPStart'],
'cfmWtPEnd': ['To', 'selectCfmWtPEnd'],
'cfmWtPSEStart': ['From', 'selectCfmWtPSEStart', 'selectCfmWtPSEEnd'],
'cfmWtPSEEnd': ['To', 'selectCfmWtPSEEnd', 'selectCfmWtPSEStart'],
'cfmCtEteStart': ['Start', 'selectCfmCtEteStart'],
'cfmCtEteEnd': ['End', 'selectCfmCtEteEnd'],
'cfmCtEteSEStart': ['Start', 'selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd'],
'cfmCtEteSEEnd': ['End', 'selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart']
cfmSeqStart: ["Start", "selectCfmSeqStart", "selectCfmSeqEnd"],
cfmSeqEnd: ["End", "selectCfmSeqEnd", "selectCfmSeqStart"],
cfmPtEteStart: ["Start", "selectCfmPtEteStart"],
cfmPtEteEnd: ["End", "selectCfmPtEteEnd"],
cfmPtEteSEStart: ["Start", "selectCfmPtEteSEStart", "selectCfmPtEteSEEnd"],
cfmPtEteSEEnd: ["End", "selectCfmPtEteSEEnd", "selectCfmPtEteSEStart"],
cfmPtPStart: ["From", "selectCfmPtPStart"],
cfmPtPEnd: ["To", "selectCfmPtPEnd"],
cfmPtPSEStart: ["From", "selectCfmPtPSEStart", "selectCfmPtPSEEnd"],
cfmPtPSEEnd: ["To", "selectCfmPtPSEEnd", "selectCfmPtPSEStart"],
cfmWtEteStart: ["Start", "selectCfmWtEteStart"],
cfmWtEteEnd: ["End", "selectCfmWtEteEnd"],
cfmWtEteSEStart: ["Start", "selectCfmWtEteSEStart", "selectCfmWtEteSEEnd"],
cfmWtEteSEEnd: ["End", "selectCfmWtEteSEEnd", "selectCfmWtEteSEStart"],
cfmWtPStart: ["From", "selectCfmWtPStart"],
cfmWtPEnd: ["To", "selectCfmWtPEnd"],
cfmWtPSEStart: ["From", "selectCfmWtPSEStart", "selectCfmWtPSEEnd"],
cfmWtPSEEnd: ["To", "selectCfmWtPSEEnd", "selectCfmWtPSEStart"],
cfmCtEteStart: ["Start", "selectCfmCtEteStart"],
cfmCtEteEnd: ["End", "selectCfmCtEteEnd"],
cfmCtEteSEStart: ["Start", "selectCfmCtEteSEStart", "selectCfmCtEteSEEnd"],
cfmCtEteSEEnd: ["End", "selectCfmCtEteSEEnd", "selectCfmCtEteSEStart"],
};
const updateSelection = (key, mainSelector, secondarySelector) => {
@@ -225,64 +408,65 @@ emitter.on('actRadioData', (newData) => {
state[mainSelector] = data;
};
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] = categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
state[mainSelector] = [data];
}
} else if (selectedRuleType.value === 'Activity duration') {
state.durationData = [data.task];
if (categoryMapping[data.category]) {
const [category, mainSelector, secondarySelector] =
categoryMapping[data.category];
if (secondarySelector) {
updateSelection(data.category, mainSelector, secondarySelector);
} else {
data.category = category;
state[mainSelector] = [data];
}
} else if (selectedRuleType.value === "Activity duration") {
state.durationData = [data.task];
}
});
emitter.on('getListSequence', (data) => {
emitter.on("getListSequence", (data) => {
switch (data.category) {
case 'cfmSeqDirectly':
case "cfmSeqDirectly":
state.selectCfmSeqDirectly = data.task;
break;
case 'cfmSeqEventually':
case "cfmSeqEventually":
state.selectCfmSeqEventually = data.task;
break;
default:
break;
}
});
emitter.on('reset', (data) => {
emitter.on("reset", (data) => {
reset();
});
// Clear data when switching radio buttons
emitter.on('isRadioChange', (data) => {
if(data) reset();
emitter.on("isRadioChange", (data) => {
if (data) reset();
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) reset();
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) reset();
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) reset();
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) reset();
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) reset();
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) reset();
});
onBeforeUnmount(() => {
emitter.off('actListData');
emitter.off('actRadioData');
emitter.off('getListSequence');
emitter.off('reset');
emitter.off('isRadioChange');
emitter.off('isRadioProcessScopeChange');
emitter.off('isRadioActSeqMoreChange');
emitter.off('isRadioActSeqFromToChange');
emitter.off("actListData");
emitter.off("actRadioData");
emitter.off("getListSequence");
emitter.off("reset");
emitter.off("isRadioChange");
emitter.off("isRadioProcessScopeChange");
emitter.off("isRadioActSeqMoreChange");
emitter.off("isRadioActSeqFromToChange");
});
</script>
<style scoped>
:deep(.disc) {
font-variation-settings:
'FILL' 1,
'wght' 100,
'GRAD' 0,
'opsz' 20
"FILL" 1,
"wght" 100,
"GRAD" 0,
"opsz" 20;
}
</style>

View File

@@ -1,99 +1,345 @@
<template>
<section class="animate-fadein w-full h-full" >
<section class="animate-fadein w-full h-full">
<!-- Have activity -->
<ActList v-if="selectedRuleType === 'Have activity'" :data="conformanceTask" :select="isSubmitTask"></ActList>
<ActList
v-if="selectedRuleType === 'Have activity'"
:data="conformanceTask"
:select="isSubmitTask"
></ActList>
<!-- Activity sequence -->
<div v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Start & End'"
class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start activity'" :select="isSubmitStartAndEnd?.[0].task" :data="cfmSeqStartData"
:category="'cfmSeqStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" class="w-1/2" />
<ActRadio :title="'End activity'" :select="isSubmitStartAndEnd?.[1].task" :data="cfmSeqEndData"
:category="'cfmSeqEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" class="w-1/2" />
<div
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start activity'"
:select="isSubmitStartAndEnd?.[0].task"
:data="cfmSeqStartData"
:category="'cfmSeqStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
class="w-1/2"
/>
<ActRadio
:title="'End activity'"
:select="isSubmitStartAndEnd?.[1].task"
:data="cfmSeqEndData"
:category="'cfmSeqEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
class="w-1/2"
/>
</div>
<!-- actSeqDrag -->
<ActSeqDrag v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'
&& selectedMode === 'Directly follows'" :data="conformanceTask" :listSeq="isSubmitCfmSeqDirectly"
:isSubmit="isSubmit" :category="'cfmSeqDirectly'"></ActSeqDrag>
<ActSeqDrag v-if="selectedRuleType === 'Activity sequence' && selectedActivitySequence === 'Sequence'
&& selectedMode === 'Eventually follows'" :data="conformanceTask" :listSeq="isSubmitCfmSeqEventually"
:isSubmit="isSubmit" :category="'cfmSeqEventually'"></ActSeqDrag>
<ActSeqDrag
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Directly follows'
"
:data="conformanceTask"
:listSeq="isSubmitCfmSeqDirectly"
:isSubmit="isSubmit"
:category="'cfmSeqDirectly'"
></ActSeqDrag>
<ActSeqDrag
v-if="
selectedRuleType === 'Activity sequence' &&
selectedActivitySequence === 'Sequence' &&
selectedMode === 'Eventually follows'
"
:data="conformanceTask"
:listSeq="isSubmitCfmSeqEventually"
:isSubmit="isSubmit"
:category="'cfmSeqEventually'"
></ActSeqDrag>
<!-- Activity duration -->
<ActRadio v-if="selectedRuleType === 'Activity duration'" :title="'Activities include'"
:select="isSubmitDurationData?.[0]" :data="conformanceTask" :category="'cfmDur'" :isSubmit="isSubmit"/>
<ActRadio
v-if="selectedRuleType === 'Activity duration'"
:title="'Activities include'"
:select="isSubmitDurationData?.[0]"
:data="conformanceTask"
:category="'cfmDur'"
:isSubmit="isSubmit"
/>
<!-- Processing time -->
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmPtEteStart?.[0].task"
:data="cfmPtEteStartData" :category="'cfmPtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmPtEteEnd?.[0].task" :data="cfmPtEteEndData"
:category="'cfmPtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmPtEteSE?.[0].task" :data="cfmPtEteSEStartData"
:category="'cfmPtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" class="w-1/2" />
<ActRadio :title="'End'" :select="isSubmitCfmPtEteSE?.[1].task" :data="cfmPtEteSEEndData"
:category="'cfmPtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" class="w-1/2" />
<!-- Processing time -->
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmPtEteStart?.[0].task"
:data="cfmPtEteStartData"
:category="'cfmPtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmPtEteEnd?.[0].task"
:data="cfmPtEteEndData"
:category="'cfmPtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmPtEteSE?.[0].task"
:data="cfmPtEteSEStartData"
:category="'cfmPtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
class="w-1/2"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmPtEteSE?.[1].task"
:data="cfmPtEteSEEndData"
:category="'cfmPtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
class="w-1/2"
/>
</div>
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :title="'From'" :select="isSubmitCfmPtPStart?.[0].task" :data="cfmPtPStartData"
:category="'cfmPtPStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :title="'To'" :select="isSubmitCfmPtPEnd?.[0].task" :data="cfmPtPEndData"
:category="'cfmPtPEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'From'" :select="isSubmitCfmPtPSE?.[0].task" :data="cfmPtPSEStartData"
class="w-1/2" :category="'cfmPtPSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'To'" :select="isSubmitCfmPtPSE?.[1].task" :data="cfmPtPSEEndData" class="w-1/2"
:category="'cfmPtPSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:title="'From'"
:select="isSubmitCfmPtPStart?.[0].task"
:data="cfmPtPStartData"
:category="'cfmPtPStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:title="'To'"
:select="isSubmitCfmPtPEnd?.[0].task"
:data="cfmPtPEndData"
:category="'cfmPtPEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'From'"
:select="isSubmitCfmPtPSE?.[0].task"
:data="cfmPtPSEStartData"
class="w-1/2"
:category="'cfmPtPSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'To'"
:select="isSubmitCfmPtPSE?.[1].task"
:data="cfmPtPSEEndData"
class="w-1/2"
:category="'cfmPtPSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<!-- Waiting time -->
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmWtEteStart?.[0].task"
:data="cfmWtEteStartData" :category="'cfmWtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmWtEteEnd?.[0].task"
:data="cfmWtEteEndData" :category="'cfmWtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmWtEteSE?.[0].task" :data="cfmWtEteSEStartData" class="w-1/2"
:category="'cfmWtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'End'" :select="isSubmitCfmWtEteSE?.[1].task" :data="cfmWtEteSEEndData" class="w-1/2"
:category="'cfmWtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmWtEteStart?.[0].task"
:data="cfmWtEteStartData"
:category="'cfmWtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmWtEteEnd?.[0].task"
:data="cfmWtEteEndData"
:category="'cfmWtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmWtEteSE?.[0].task"
:data="cfmWtEteSEStartData"
class="w-1/2"
:category="'cfmWtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmWtEteSE?.[1].task"
:data="cfmWtEteSEEndData"
class="w-1/2"
:category="'cfmWtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :title="'From'" :select="isSubmitCfmWtPStart?.[0].task" :data="cfmWtPStartData"
:category="'cfmWtPStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :title="'To'" :select="isSubmitCfmWtPEnd?.[0].task"
:data="cfmWtPEndData" :category="'cfmWtPEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" class="flex justify-between items-center w-full h-full">
<ActRadio :title="'From'" :select="isSubmitCfmWtPSE?.[0].task" :data="cfmWtPSEStartData"
class="w-1/2" :category="'cfmWtPSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'To'" :select="isSubmitCfmWtPSE?.[1].task" :data="cfmWtPSEEndData"
class="w-1/2" :category="'cfmWtPSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:title="'From'"
:select="isSubmitCfmWtPStart?.[0].task"
:data="cfmWtPStartData"
:category="'cfmWtPStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:title="'To'"
:select="isSubmitCfmWtPEnd?.[0].task"
:data="cfmWtPEndData"
:category="'cfmWtPEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'From'"
:select="isSubmitCfmWtPSE?.[0].task"
:data="cfmWtPSEStartData"
class="w-1/2"
:category="'cfmWtPSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'To'"
:select="isSubmitCfmWtPSE?.[1].task"
:data="cfmWtPSEEndData"
class="w-1/2"
:category="'cfmWtPSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
<!-- Cycle time -->
<ActRadio v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :title="'Start'" :select="isSubmitCfmCtEteStart?.[0].task"
:data="cfmCtEteStartData" :category="'cfmCtEteStart'" :isSubmit="isSubmit" />
<ActRadio v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :title="'End'" :select="isSubmitCfmCtEteEnd?.[0].task" :data="cfmCtEteEndData"
:category="'cfmCtEteEnd'" :isSubmit="isSubmit" />
<div v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end' && selectedActSeqMore === 'Start & End'"
class="flex justify-between items-center w-full h-full">
<ActRadio :title="'Start'" :select="isSubmitCfmCtEteSE?.[0].task" :data="cfmCtEteSEStartData" class="w-1/2"
:category="'cfmCtEteSEStart'" :task="taskStart" :isSubmit="isSubmit" @selected-task="selectStart" />
<ActRadio :title="'End'" :select="isSubmitCfmCtEteSE?.[1].task" :data="cfmCtEteSEEndData" class="w-1/2"
:category="'cfmCtEteSEEnd'" :task="taskEnd" :isSubmit="isSubmit" @selected-task="selectEnd" />
<ActRadio
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:title="'Start'"
:select="isSubmitCfmCtEteStart?.[0].task"
:data="cfmCtEteStartData"
:category="'cfmCtEteStart'"
:isSubmit="isSubmit"
/>
<ActRadio
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:title="'End'"
:select="isSubmitCfmCtEteEnd?.[0].task"
:data="cfmCtEteEndData"
:category="'cfmCtEteEnd'"
:isSubmit="isSubmit"
/>
<div
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
class="flex justify-between items-center w-full h-full"
>
<ActRadio
:title="'Start'"
:select="isSubmitCfmCtEteSE?.[0].task"
:data="cfmCtEteSEStartData"
class="w-1/2"
:category="'cfmCtEteSEStart'"
:task="taskStart"
:isSubmit="isSubmit"
@selected-task="selectStart"
/>
<ActRadio
:title="'End'"
:select="isSubmitCfmCtEteSE?.[1].task"
:data="cfmCtEteSEEndData"
class="w-1/2"
:category="'cfmCtEteSEEnd'"
:task="taskEnd"
:isSubmit="isSubmit"
@selected-task="selectEnd"
/>
</div>
</section>
</template>
@@ -110,31 +356,75 @@
* check result statistics.
*/
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import ActList from './ActList.vue';
import ActRadio from './ActRadio.vue';
import ActSeqDrag from './ActSeqDrag.vue';
import { ref, computed, watch } from "vue";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import ActList from "./ActList.vue";
import ActRadio from "./ActRadio.vue";
import ActSeqDrag from "./ActSeqDrag.vue";
const loadingStore = useLoadingStore();
const conformanceStore = useConformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope, selectedActSeqMore,
selectedActSeqFromTo, conformanceTask, cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE,
cfmPtPStart, cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart, cfmWtPEnd,
cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, isStartSelected, isEndSelected
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
conformanceTask,
cfmSeqStart,
cfmSeqEnd,
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
isStartSelected,
isEndSelected,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmit', 'isSubmitTask', 'isSubmitStartAndEnd', 'isSubmitCfmSeqDirectly', 'isSubmitCfmSeqEventually',
'isSubmitDurationData', 'isSubmitCfmPtEteStart', 'isSubmitCfmPtEteEnd', 'isSubmitCfmPtEteSE',
'isSubmitCfmPtPStart', 'isSubmitCfmPtPEnd', 'isSubmitCfmPtPSE', 'isSubmitCfmWtEteStart',
'isSubmitCfmWtEteEnd', 'isSubmitCfmWtEteSE', 'isSubmitCfmWtPStart', 'isSubmitCfmWtPEnd',
'isSubmitCfmWtPSE', 'isSubmitCfmCtEteStart', 'isSubmitCfmCtEteEnd', 'isSubmitCfmCtEteSE',
'isSubmitShowDataSeq', 'isSubmitShowDataPtEte', 'isSubmitShowDataPtP', 'isSubmitShowDataWtEte',
'isSubmitShowDataWtP', 'isSubmitShowDataCt'
const props = defineProps([
"isSubmit",
"isSubmitTask",
"isSubmitStartAndEnd",
"isSubmitCfmSeqDirectly",
"isSubmitCfmSeqEventually",
"isSubmitDurationData",
"isSubmitCfmPtEteStart",
"isSubmitCfmPtEteEnd",
"isSubmitCfmPtEteSE",
"isSubmitCfmPtPStart",
"isSubmitCfmPtPEnd",
"isSubmitCfmPtPSE",
"isSubmitCfmWtEteStart",
"isSubmitCfmWtEteEnd",
"isSubmitCfmWtEteSE",
"isSubmitCfmWtPStart",
"isSubmitCfmWtPEnd",
"isSubmitCfmWtPSE",
"isSubmitCfmCtEteStart",
"isSubmitCfmCtEteEnd",
"isSubmitCfmCtEteSE",
"isSubmitShowDataSeq",
"isSubmitShowDataPtEte",
"isSubmitShowDataPtP",
"isSubmitShowDataWtEte",
"isSubmitShowDataWtP",
"isSubmitShowDataCt",
]);
const task = ref(null);
@@ -143,112 +433,166 @@ const taskEnd = ref(null);
// Activity sequence
const cfmSeqStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isEndSelected.value ? setSeqStartAndEndData(cfmSeqEnd.value, 'sources', task.value) : cfmSeqStart.value.map(i => i.label);
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataSeq.task;
return isEndSelected.value
? setSeqStartAndEndData(cfmSeqEnd.value, "sources", task.value)
: cfmSeqStart.value.map((i) => i.label);
});
const cfmSeqEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataSeq.task;
return isStartSelected.value ? setSeqStartAndEndData(cfmSeqStart.value, 'sinks', task.value) : cfmSeqEnd.value.map(i => i.label);
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataSeq.task;
return isStartSelected.value
? setSeqStartAndEndData(cfmSeqStart.value, "sinks", task.value)
: cfmSeqEnd.value.map((i) => i.label);
});
// Processing time
const cfmPtEteStartData = computed(() => {
return cfmPtEteStart.value.map(i => i.task);
return cfmPtEteStart.value.map((i) => i.task);
});
const cfmPtEteEndData = computed(() => {
return cfmPtEteEnd.value.map(i => i.task);
return cfmPtEteEnd.value.map((i) => i.task);
});
const cfmPtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'end', task.value) : setTaskData(cfmPtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtEte.task;
return isEndSelected.value
? setStartAndEndData(cfmPtEteSE.value, "end", task.value)
: setTaskData(cfmPtEteSE.value, "start");
});
const cfmPtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmPtEteSE.value, 'start', task.value) : setTaskData(cfmPtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtEte.task;
return isStartSelected.value
? setStartAndEndData(cfmPtEteSE.value, "start", task.value)
: setTaskData(cfmPtEteSE.value, "end");
});
const cfmPtPStartData = computed(() => {
return cfmPtPStart.value.map(i => i.task);
return cfmPtPStart.value.map((i) => i.task);
});
const cfmPtPEndData = computed(() => {
return cfmPtPEnd.value.map(i => i.task);
return cfmPtPEnd.value.map((i) => i.task);
});
const cfmPtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isEndSelected.value ? setStartAndEndData(cfmPtPSE.value, 'end', task.value) : setTaskData(cfmPtPSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtP.task;
return isEndSelected.value
? setStartAndEndData(cfmPtPSE.value, "end", task.value)
: setTaskData(cfmPtPSE.value, "start");
});
const cfmPtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataPtP.task;
return isStartSelected.value ? setStartAndEndData(cfmPtPSE.value, 'start', task.value) : setTaskData(cfmPtPSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataPtP.task;
return isStartSelected.value
? setStartAndEndData(cfmPtPSE.value, "start", task.value)
: setTaskData(cfmPtPSE.value, "end");
});
// Waiting time
const cfmWtEteStartData = computed(() => {
return cfmWtEteStart.value.map(i => i.task);
return cfmWtEteStart.value.map((i) => i.task);
});
const cfmWtEteEndData = computed(() => {
return cfmWtEteEnd.value.map(i => i.task);
return cfmWtEteEnd.value.map((i) => i.task);
});
const cfmWtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isEndSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'end', task.value) : setTaskData(cfmWtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtEte.task;
return isEndSelected.value
? setStartAndEndData(cfmWtEteSE.value, "end", task.value)
: setTaskData(cfmWtEteSE.value, "start");
});
const cfmWtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtEte.task;
return isStartSelected.value ? setStartAndEndData(cfmWtEteSE.value, 'start', task.value) : setTaskData(cfmWtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtEte.task;
return isStartSelected.value
? setStartAndEndData(cfmWtEteSE.value, "start", task.value)
: setTaskData(cfmWtEteSE.value, "end");
});
const cfmWtPStartData = computed(() => {
return cfmWtPStart.value.map(i => i.task);
return cfmWtPStart.value.map((i) => i.task);
});
const cfmWtPEndData = computed(() => {
return cfmWtPEnd.value.map(i => i.task);
return cfmWtPEnd.value.map((i) => i.task);
});
const cfmWtPSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isEndSelected.value ? setStartAndEndData(cfmWtPSE.value, 'end', task.value) : setTaskData(cfmWtPSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtP.task;
return isEndSelected.value
? setStartAndEndData(cfmWtPSE.value, "end", task.value)
: setTaskData(cfmWtPSE.value, "start");
});
const cfmWtPSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataWtP.task;
return isStartSelected.value ? setStartAndEndData(cfmWtPSE.value, 'start', task.value) : setTaskData(cfmWtPSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataWtP.task;
return isStartSelected.value
? setStartAndEndData(cfmWtPSE.value, "start", task.value)
: setTaskData(cfmWtPSE.value, "end");
});
// Cycle time
const cfmCtEteStartData = computed(() => {
return cfmCtEteStart.value.map(i => i.task);
return cfmCtEteStart.value.map((i) => i.task);
});
const cfmCtEteEndData = computed(() => {
return cfmCtEteEnd.value.map(i => i.task);
return cfmCtEteEnd.value.map((i) => i.task);
});
const cfmCtEteSEStartData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isEndSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'end', task.value) : setTaskData(cfmCtEteSE.value, 'start');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataCt.task;
return isEndSelected.value
? setStartAndEndData(cfmCtEteSE.value, "end", task.value)
: setTaskData(cfmCtEteSE.value, "start");
});
const cfmCtEteSEEndData = computed(() => {
if(props.isSubmit && task.value === null) task.value = props.isSubmitShowDataCt.task;
return isStartSelected.value ? setStartAndEndData(cfmCtEteSE.value, 'start', task.value) : setTaskData(cfmCtEteSE.value, 'end');
if (props.isSubmit && task.value === null)
task.value = props.isSubmitShowDataCt.task;
return isStartSelected.value
? setStartAndEndData(cfmCtEteSE.value, "start", task.value)
: setTaskData(cfmCtEteSE.value, "end");
});
// Watchers - Fix issue where saved rule files could not be re-edited
watch(() => props.isSubmitShowDataSeq, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataPtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtEte, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataWtP, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(() => props.isSubmitShowDataCt, (newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
});
watch(
() => props.isSubmitShowDataSeq,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataPtEte,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataPtP,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataWtEte,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataWtP,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
watch(
() => props.isSubmitShowDataCt,
(newValue) => {
taskStart.value = newValue.taskStart;
taskEnd.value = newValue.taskEnd;
},
);
/**
* Sets the start and end radio data.
@@ -257,7 +601,7 @@ watch(() => props.isSubmitShowDataCt, (newValue) => {
* @returns {array}
*/
function setTaskData(data, category) {
let newData = data.map(i => i[category]);
let newData = data.map((i) => i[category]);
newData = [...new Set(newData)]; // Set is a collection type that only stores unique values.
return newData;
}
@@ -269,13 +613,15 @@ function setTaskData(data, category) {
* @returns {array}
*/
function setStartAndEndData(data, category, taskVal) {
let oppositeCategory = '';
if (category === 'start') {
oppositeCategory = 'end';
let oppositeCategory = "";
if (category === "start") {
oppositeCategory = "end";
} else {
oppositeCategory = 'start';
};
let newData = data.filter(i => i[category] === taskVal).map(i => i[oppositeCategory]);
oppositeCategory = "start";
}
let newData = data
.filter((i) => i[category] === taskVal)
.map((i) => i[oppositeCategory]);
newData = [...new Set(newData)];
return newData;
}
@@ -287,7 +633,7 @@ function setStartAndEndData(data, category, taskVal) {
* @returns {array}
*/
function setSeqStartAndEndData(data, category, taskVal) {
let newData = data.filter(i => i.label === taskVal).map(i => i[category]);
let newData = data.filter((i) => i.label === taskVal).map((i) => i[category]);
newData = [...new Set(...newData)];
return newData;
}
@@ -297,16 +643,16 @@ function setSeqStartAndEndData(data, category, taskVal) {
*/
function selectStart(e) {
taskStart.value = e;
if(isStartSelected.value === null || isStartSelected.value === true){
if (isStartSelected.value === null || isStartSelected.value === true) {
isStartSelected.value = true;
isEndSelected.value = false;
task.value = e;
taskEnd.value = null;
emitter.emit('sratrAndEndToStart', {
emitter.emit("sratrAndEndToStart", {
start: true,
end: false,
});
};
}
}
/**
* select End list's task
@@ -314,12 +660,12 @@ function selectStart(e) {
*/
function selectEnd(e) {
taskEnd.value = e;
if(isEndSelected.value === null || isEndSelected.value === true){
if (isEndSelected.value === null || isEndSelected.value === true) {
isEndSelected.value = true;
isStartSelected.value = false;
task.value = e;
taskStart.value = null;
emitter.emit('sratrAndEndToStart', {
emitter.emit("sratrAndEndToStart", {
start: false,
end: true,
});
@@ -340,22 +686,23 @@ function reset() {
* @param {boolean} data - Whether data should be restored from submission state.
*/
function setResetData(data) {
if(data) {
if(props.isSubmit) {
if (data) {
if (props.isSubmit) {
switch (selectedRuleType.value) {
case 'Activity sequence':
case "Activity sequence":
task.value = props.isSubmitShowDataSeq.task;
isStartSelected.value = props.isSubmitShowDataSeq.isStartSelected;
isEndSelected.value = props.isSubmitShowDataSeq.isEndSelected;
break;
case 'Processing time':
case "Processing time":
switch (selectedProcessScope.value) {
case 'End to end':
case "End to end":
task.value = props.isSubmitShowDataPtEte.task;
isStartSelected.value = props.isSubmitShowDataPtEte.isStartSelected;
isStartSelected.value =
props.isSubmitShowDataPtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtEte.isEndSelected;
break;
case 'Partial':
case "Partial":
task.value = props.isSubmitShowDataPtP.task;
isStartSelected.value = props.isSubmitShowDataPtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtP.isEndSelected;
@@ -364,23 +711,24 @@ function setResetData(data) {
break;
}
break;
case 'Waiting time':
case "Waiting time":
switch (selectedProcessScope.value) {
case 'End to end':
task.value = props.isSubmitShowDataWtEte.task;
isStartSelected.value = props.isSubmitShowDataWtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
break;
case 'Partial':
task.value = props.isSubmitShowDataWtP.task;
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case 'Cycle time':
case "End to end":
task.value = props.isSubmitShowDataWtEte.task;
isStartSelected.value =
props.isSubmitShowDataWtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtEte.isEndSelected;
break;
case "Partial":
task.value = props.isSubmitShowDataWtP.task;
isStartSelected.value = props.isSubmitShowDataWtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataWtP.isEndSelected;
break;
default:
break;
}
break;
case "Cycle time":
task.value = props.isSubmitShowDataCt.task;
isStartSelected.value = props.isSubmitShowDataCt.isStartSelected;
isEndSelected.value = props.isSubmitShowDataCt.isEndSelected;
@@ -395,28 +743,28 @@ function setResetData(data) {
}
// created() logic
emitter.on('isRadioChange', (data) => {
emitter.on("isRadioChange", (data) => {
setResetData(data);
});
emitter.on('isRadioSeqChange', (data) => {
emitter.on("isRadioSeqChange", (data) => {
setResetData(data);
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
setResetData(data);
};
}
});
emitter.on('reset', data => {
emitter.on("reset", (data) => {
reset();
});
</script>

View File

@@ -1,71 +1,226 @@
<template>
<div class="mt-2 mb-12" v-if="selectedRuleType === 'Activity duration' || selectedRuleType === 'Waiting time'
|| selectedRuleType === 'Processing time' || selectedRuleType === 'Cycle time'">
<p class="h2">Time Range</p>
<div class=" text-sm leading-normal">
<div
class="mt-2 mb-12"
v-if="
selectedRuleType === 'Activity duration' ||
selectedRuleType === 'Waiting time' ||
selectedRuleType === 'Processing time' ||
selectedRuleType === 'Cycle time'
"
>
<p class="h2">Time Range</p>
<div class="text-sm leading-normal">
<!-- Activity duration -->
<TimeRangeDuration
v-if="selectedRuleType === 'Activity duration'" :time="state.timeDuration" :select="isSubmitDurationTime" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Processing time -->
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmPtEteAll" :select="isSubmitTimeCfmPtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmPtEteStart" :select="isSubmitTimeCfmPtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmPtEteEnd" :select="isSubmitTimeCfmPtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmPtEteSE" :select="isSubmitTimeCfmPtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmPtPStart" :select="isSubmitTimeCfmPtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmPtPEnd" :select="isSubmitTimeCfmPtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Processing time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmPtPSE" :select="isSubmitTimeCfmPtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Waiting time -->
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmWtEteAll" :select="isSubmitTimeCfmWtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmWtEteStart" :select="isSubmitTimeCfmWtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmWtEteEnd" :select="isSubmitTimeCfmWtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmWtEteSE" :select="isSubmitTimeCfmWtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From'" :time="state.timeCfmWtPStart" :select="isSubmitTimeCfmWtPStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'To'" :time="state.timeCfmWtPEnd" :select="isSubmitTimeCfmWtPEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Waiting time' && selectedProcessScope === 'Partial'
&& selectedActSeqFromTo === 'From & To'" :time="state.timeCfmWtPSE" :select="isSubmitTimeCfmWtPSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<!-- Cycle time -->
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'All'" :time="state.timeCfmCtEteAll" :select="isSubmitTimeCfmCtEteAll" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start'" :time="state.timeCfmCtEteStart" :select="isSubmitTimeCfmCtEteStart" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'End'" :time="state.timeCfmCtEteEnd" :select="isSubmitTimeCfmCtEteEnd" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
<TimeRangeDuration v-if="selectedRuleType === 'Cycle time' && selectedProcessScope === 'End to end'
&& selectedActSeqMore === 'Start & End'" :time="state.timeCfmCtEteSE" :select="isSubmitTimeCfmCtEteSE" @min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds" />
v-if="selectedRuleType === 'Activity duration'"
:time="state.timeDuration"
:select="isSubmitDurationTime"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Processing time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmPtEteAll"
:select="isSubmitTimeCfmPtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmPtEteStart"
:select="isSubmitTimeCfmPtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmPtEteEnd"
:select="isSubmitTimeCfmPtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmPtEteSE"
:select="isSubmitTimeCfmPtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:time="state.timeCfmPtPStart"
:select="isSubmitTimeCfmPtPStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:time="state.timeCfmPtPEnd"
:select="isSubmitTimeCfmPtPEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Processing time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:time="state.timeCfmPtPSE"
:select="isSubmitTimeCfmPtPSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Waiting time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmWtEteAll"
:select="isSubmitTimeCfmWtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmWtEteStart"
:select="isSubmitTimeCfmWtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmWtEteEnd"
:select="isSubmitTimeCfmWtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmWtEteSE"
:select="isSubmitTimeCfmWtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From'
"
:time="state.timeCfmWtPStart"
:select="isSubmitTimeCfmWtPStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'To'
"
:time="state.timeCfmWtPEnd"
:select="isSubmitTimeCfmWtPEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Waiting time' &&
selectedProcessScope === 'Partial' &&
selectedActSeqFromTo === 'From & To'
"
:time="state.timeCfmWtPSE"
:select="isSubmitTimeCfmWtPSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<!-- Cycle time -->
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'All'
"
:time="state.timeCfmCtEteAll"
:select="isSubmitTimeCfmCtEteAll"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start'
"
:time="state.timeCfmCtEteStart"
:select="isSubmitTimeCfmCtEteStart"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'End'
"
:time="state.timeCfmCtEteEnd"
:select="isSubmitTimeCfmCtEteEnd"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
<TimeRangeDuration
v-if="
selectedRuleType === 'Cycle time' &&
selectedProcessScope === 'End to end' &&
selectedActSeqMore === 'Start & End'
"
:time="state.timeCfmCtEteSE"
:select="isSubmitTimeCfmCtEteSE"
@min-total-seconds="minTotalSeconds"
@max-total-seconds="maxTotalSeconds"
/>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
@@ -80,30 +235,67 @@
* configuration with calendar inputs.
*/
import { reactive } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import emitter from '@/utils/emitter';
import TimeRangeDuration from '@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue';
import { reactive } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import emitter from "@/utils/emitter";
import TimeRangeDuration from "@/components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration.vue";
const conformanceStore = useConformanceStore();
const { selectedRuleType, selectedActivitySequence, selectedMode, selectedProcessScope,
selectedActSeqMore, selectedActSeqFromTo, conformanceAllTasks, conformanceTask,
cfmSeqStart, cfmSeqEnd, cfmPtEteStart, cfmPtEteEnd, cfmPtEteSE, cfmPtPStart,
cfmPtPEnd, cfmPtPSE, cfmWtEteStart, cfmWtEteEnd, cfmWtEteSE, cfmWtPStart,
cfmWtPEnd, cfmWtPSE, cfmCtEteStart, cfmCtEteEnd, cfmCtEteSE, cfmPtEteWhole,
cfmWtEteWhole, cfmCtEteWhole
const {
selectedRuleType,
selectedActivitySequence,
selectedMode,
selectedProcessScope,
selectedActSeqMore,
selectedActSeqFromTo,
conformanceAllTasks,
conformanceTask,
cfmSeqStart,
cfmSeqEnd,
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
cfmPtEteWhole,
cfmWtEteWhole,
cfmCtEteWhole,
} = storeToRefs(conformanceStore);
const props = defineProps(['isSubmitDurationTime', 'isSubmitTimeCfmPtEteAll', 'isSubmitTimeCfmPtEteStart',
'isSubmitTimeCfmPtEteEnd', 'isSubmitTimeCfmPtEteSE', 'isSubmitTimeCfmPtPStart',
'isSubmitTimeCfmPtPEnd', 'isSubmitTimeCfmPtPSE', 'isSubmitTimeCfmWtEteAll',
'isSubmitTimeCfmWtEteStart', 'isSubmitTimeCfmWtEteEnd', 'isSubmitTimeCfmWtEteSE',
'isSubmitTimeCfmWtPStart', 'isSubmitTimeCfmWtPEnd', 'isSubmitTimeCfmWtPSE', 'isSubmitTimeCfmCtEteAll',
'isSubmitTimeCfmCtEteStart', 'isSubmitTimeCfmCtEteEnd', 'isSubmitTimeCfmCtEteSE'
const props = defineProps([
"isSubmitDurationTime",
"isSubmitTimeCfmPtEteAll",
"isSubmitTimeCfmPtEteStart",
"isSubmitTimeCfmPtEteEnd",
"isSubmitTimeCfmPtEteSE",
"isSubmitTimeCfmPtPStart",
"isSubmitTimeCfmPtPEnd",
"isSubmitTimeCfmPtPSE",
"isSubmitTimeCfmWtEteAll",
"isSubmitTimeCfmWtEteStart",
"isSubmitTimeCfmWtEteEnd",
"isSubmitTimeCfmWtEteSE",
"isSubmitTimeCfmWtPStart",
"isSubmitTimeCfmWtPEnd",
"isSubmitTimeCfmWtPSE",
"isSubmitTimeCfmCtEteAll",
"isSubmitTimeCfmCtEteStart",
"isSubmitTimeCfmCtEteEnd",
"isSubmitTimeCfmCtEteSE",
]);
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const emit = defineEmits(["min-total-seconds", "max-total-seconds"]);
const state = reactive({
timeDuration: null, // Activity duration
@@ -164,14 +356,14 @@ const storeRefs = {
* @param {number} e - The minimum total seconds.
*/
function minTotalSeconds(e) {
emit('min-total-seconds', e);
emit("min-total-seconds", e);
}
/**
* get min total seconds
* @param {number} e - The maximum total seconds.
*/
function maxTotalSeconds(e) {
emit('max-total-seconds', e);
emit("max-total-seconds", e);
}
/**
* get Time Range(duration)
@@ -182,35 +374,35 @@ function maxTotalSeconds(e) {
* @returns {object} {min:12, max:345}
*/
function getDurationTime(data, category, task, taskTwo) {
let result = {min:0, max:0};
let result = { min: 0, max: 0 };
switch (category) {
case 'act':
data.forEach(i => {
if(i.label === task) {
case "act":
data.forEach((i) => {
if (i.label === task) {
result = i.duration;
}
});
break;
case 'single':
data.forEach(i => {
if(i.task === task) {
case "single":
data.forEach((i) => {
if (i.task === task) {
result = i.time;
}
});
break;
case 'double':
data.forEach(i => {
if(i.start === task && i.end === taskTwo) {
case "double":
data.forEach((i) => {
if (i.start === task && i.end === taskTwo) {
result = i.time;
}
});
break;
case 'all':
case "all":
result = data;
break
break;
default:
break;
};
}
return result;
}
/**
@@ -249,148 +441,235 @@ function reset() {
}
// created() logic
emitter.on('actRadioData', (data) => {
emitter.on("actRadioData", (data) => {
const category = data.category;
const task = data.task;
const handleDoubleSelection = (startKey, endKey, timeKey, durationType) => {
state[startKey] = task;
state[timeKey] = { min: 0, max: 0 };
if (state[endKey]) {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'double', task, state[endKey]);
}
state[startKey] = task;
state[timeKey] = { min: 0, max: 0 };
if (state[endKey]) {
state[timeKey] = getDurationTime(
storeRefs[durationType].value,
"double",
task,
state[endKey],
);
}
};
const handleSingleSelection = (key, timeKey, durationType) => {
state[timeKey] = getDurationTime(storeRefs[durationType].value, 'single', task);
state[timeKey] = getDurationTime(
storeRefs[durationType].value,
"single",
task,
);
};
switch (category) {
// Activity duration
case 'cfmDur':
state.timeDuration = getDurationTime(conformanceAllTasks.value, 'act', task);
break;
// Processing time
case 'cfmPtEteStart':
handleSingleSelection('cfmPtEteStart', 'timeCfmPtEteStart', 'cfmPtEteStart');
break;
case 'cfmPtEteEnd':
handleSingleSelection('cfmPtEteEnd', 'timeCfmPtEteEnd', 'cfmPtEteEnd');
break;
case 'cfmPtEteSEStart':
handleDoubleSelection('selectCfmPtEteSEStart', 'selectCfmPtEteSEEnd', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtEteSEEnd':
handleDoubleSelection('selectCfmPtEteSEEnd', 'selectCfmPtEteSEStart', 'timeCfmPtEteSE', 'cfmPtEteSE');
break;
case 'cfmPtPStart':
handleSingleSelection('cfmPtPStart', 'timeCfmPtPStart', 'cfmPtPStart');
break;
case 'cfmPtPEnd':
handleSingleSelection('cfmPtPEnd', 'timeCfmPtPEnd', 'cfmPtPEnd');
break;
case 'cfmPtPSEStart':
handleDoubleSelection('selectCfmPtPSEStart', 'selectCfmPtPSEEnd', 'timeCfmPtPSE', 'cfmPtPSE');
break;
case 'cfmPtPSEEnd':
handleDoubleSelection('selectCfmPtPSEEnd', 'selectCfmPtPSEStart', 'timeCfmPtPSE', 'cfmPtPSE');
break;
// Waiting time
case 'cfmWtEteStart':
handleSingleSelection('cfmWtEteStart', 'timeCfmWtEteStart', 'cfmWtEteStart');
break;
case 'cfmWtEteEnd':
handleSingleSelection('cfmWtEteEnd', 'timeCfmWtEteEnd', 'cfmWtEteEnd');
break;
case 'cfmWtEteSEStart':
handleDoubleSelection('selectCfmWtEteSEStart', 'selectCfmWtEteSEEnd', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtEteSEEnd':
handleDoubleSelection('selectCfmWtEteSEEnd', 'selectCfmWtEteSEStart', 'timeCfmWtEteSE', 'cfmWtEteSE');
break;
case 'cfmWtPStart':
handleSingleSelection('cfmWtPStart', 'timeCfmWtPStart', 'cfmWtPStart');
break;
case 'cfmWtPEnd':
handleSingleSelection('cfmWtPEnd', 'timeCfmWtPEnd', 'cfmWtPEnd');
break;
case 'cfmWtPSEStart':
handleDoubleSelection('selectCfmWtPSEStart', 'selectCfmWtPSEEnd', 'timeCfmWtPSE', 'cfmWtPSE');
break;
case 'cfmWtPSEEnd':
handleDoubleSelection('selectCfmWtPSEEnd', 'selectCfmWtPSEStart', 'timeCfmWtPSE', 'cfmWtPSE');
break;
// Cycle time
case 'cfmCtEteStart':
handleSingleSelection('cfmCtEteStart', 'timeCfmCtEteStart', 'cfmCtEteStart');
break;
case 'cfmCtEteEnd':
handleSingleSelection('cfmCtEteEnd', 'timeCfmCtEteEnd', 'cfmCtEteEnd');
break;
case 'cfmCtEteSEStart':
handleDoubleSelection('selectCfmCtEteSEStart', 'selectCfmCtEteSEEnd', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
case 'cfmCtEteSEEnd':
handleDoubleSelection('selectCfmCtEteSEEnd', 'selectCfmCtEteSEStart', 'timeCfmCtEteSE', 'cfmCtEteSE');
break;
default:
break;
};
});
emitter.on('reset', (data) => {
reset();
});
emitter.on('isRadioChange', (data) => {
if(data) {
reset();
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
break;
default:
break;
};
// Activity duration
case "cfmDur":
state.timeDuration = getDurationTime(
conformanceAllTasks.value,
"act",
task,
);
break;
// Processing time
case "cfmPtEteStart":
handleSingleSelection(
"cfmPtEteStart",
"timeCfmPtEteStart",
"cfmPtEteStart",
);
break;
case "cfmPtEteEnd":
handleSingleSelection("cfmPtEteEnd", "timeCfmPtEteEnd", "cfmPtEteEnd");
break;
case "cfmPtEteSEStart":
handleDoubleSelection(
"selectCfmPtEteSEStart",
"selectCfmPtEteSEEnd",
"timeCfmPtEteSE",
"cfmPtEteSE",
);
break;
case "cfmPtEteSEEnd":
handleDoubleSelection(
"selectCfmPtEteSEEnd",
"selectCfmPtEteSEStart",
"timeCfmPtEteSE",
"cfmPtEteSE",
);
break;
case "cfmPtPStart":
handleSingleSelection("cfmPtPStart", "timeCfmPtPStart", "cfmPtPStart");
break;
case "cfmPtPEnd":
handleSingleSelection("cfmPtPEnd", "timeCfmPtPEnd", "cfmPtPEnd");
break;
case "cfmPtPSEStart":
handleDoubleSelection(
"selectCfmPtPSEStart",
"selectCfmPtPSEEnd",
"timeCfmPtPSE",
"cfmPtPSE",
);
break;
case "cfmPtPSEEnd":
handleDoubleSelection(
"selectCfmPtPSEEnd",
"selectCfmPtPSEStart",
"timeCfmPtPSE",
"cfmPtPSE",
);
break;
// Waiting time
case "cfmWtEteStart":
handleSingleSelection(
"cfmWtEteStart",
"timeCfmWtEteStart",
"cfmWtEteStart",
);
break;
case "cfmWtEteEnd":
handleSingleSelection("cfmWtEteEnd", "timeCfmWtEteEnd", "cfmWtEteEnd");
break;
case "cfmWtEteSEStart":
handleDoubleSelection(
"selectCfmWtEteSEStart",
"selectCfmWtEteSEEnd",
"timeCfmWtEteSE",
"cfmWtEteSE",
);
break;
case "cfmWtEteSEEnd":
handleDoubleSelection(
"selectCfmWtEteSEEnd",
"selectCfmWtEteSEStart",
"timeCfmWtEteSE",
"cfmWtEteSE",
);
break;
case "cfmWtPStart":
handleSingleSelection("cfmWtPStart", "timeCfmWtPStart", "cfmWtPStart");
break;
case "cfmWtPEnd":
handleSingleSelection("cfmWtPEnd", "timeCfmWtPEnd", "cfmWtPEnd");
break;
case "cfmWtPSEStart":
handleDoubleSelection(
"selectCfmWtPSEStart",
"selectCfmWtPSEEnd",
"timeCfmWtPSE",
"cfmWtPSE",
);
break;
case "cfmWtPSEEnd":
handleDoubleSelection(
"selectCfmWtPSEEnd",
"selectCfmWtPSEStart",
"timeCfmWtPSE",
"cfmWtPSE",
);
break;
// Cycle time
case "cfmCtEteStart":
handleSingleSelection(
"cfmCtEteStart",
"timeCfmCtEteStart",
"cfmCtEteStart",
);
break;
case "cfmCtEteEnd":
handleSingleSelection("cfmCtEteEnd", "timeCfmCtEteEnd", "cfmCtEteEnd");
break;
case "cfmCtEteSEStart":
handleDoubleSelection(
"selectCfmCtEteSEStart",
"selectCfmCtEteSEEnd",
"timeCfmCtEteSE",
"cfmCtEteSE",
);
break;
case "cfmCtEteSEEnd":
handleDoubleSelection(
"selectCfmCtEteSEEnd",
"selectCfmCtEteSEStart",
"timeCfmCtEteSE",
"cfmCtEteSE",
);
break;
default:
break;
}
});
emitter.on('isRadioProcessScopeChange', (data) => {
if(data) {
reset();
};
emitter.on("reset", (data) => {
reset();
});
emitter.on('isRadioActSeqMoreChange', (data) => {
if(data) {
if(selectedActSeqMore.value === 'All') {
emitter.on("isRadioChange", (data) => {
if (data) {
reset();
switch (selectedRuleType.value) {
case "Processing time":
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, "all");
state.timeCfmPtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmPtEteAll),
);
break;
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmWtEteAll),
);
break;
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmCtEteAll),
);
break;
default:
break;
}
}
});
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) {
reset();
}
});
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) {
if (selectedActSeqMore.value === "All") {
switch (selectedRuleType.value) {
case 'Processing time':
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, 'all');
state.timeCfmPtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmPtEteAll));
case "Processing time":
state.timeCfmPtEteAll = getDurationTime(cfmPtEteWhole.value, "all");
state.timeCfmPtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmPtEteAll),
);
break;
case 'Waiting time':
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, 'all');
state.timeCfmWtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmWtEteAll));
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmWtEteAll),
);
break;
case 'Cycle time':
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, 'all');
state.timeCfmCtEteAllDefault = JSON.parse(JSON.stringify(state.timeCfmCtEteAll));
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = JSON.parse(
JSON.stringify(state.timeCfmCtEteAll),
);
break;
default:
break;
};
}else reset();
};
}
} else reset();
}
});
emitter.on('isRadioActSeqFromToChange', (data) => {
if(data) {
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
reset();
};
}
});
</script>

View File

@@ -1,10 +1,19 @@
<template>
<ul class="space-y-2" id="cyp-conformance-result-arrow">
<li class="flex justify-start items-center pr-4" v-for="(act, index) in data" :key="index" :title="act">
<li
class="flex justify-start items-center pr-4"
v-for="(act, index) in data"
:key="index"
:title="act"
>
<span class="material-symbols-outlined text-primary mr-2">
arrow_circle_down
</span>
<p class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</p>
<p
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>
{{ act }}
</p>
</li>
</ul>
</template>
@@ -20,5 +29,5 @@
* sequences.
*/
defineProps(['data', 'select']);
defineProps(["data", "select"]);
</script>

View File

@@ -1,10 +1,19 @@
<template>
<ul class="space-y-2" id="cyp-conformance-result-check">
<li class="flex justify-start items-center pr-4" v-for="(act, index) in datadata" :key="index" :title="act">
<li
class="flex justify-start items-center pr-4"
v-for="(act, index) in datadata"
:key="index"
:title="act"
>
<span class="material-symbols-outlined text-primary mr-2">
check_circle
</span>
<p class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden">{{ act }}</p>
<p
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden"
>
{{ act }}
</p>
</li>
</ul>
</template>
@@ -20,20 +29,26 @@
* matched activities.
*/
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(['data', 'select']);
const props = defineProps(["data", "select"]);
const datadata = ref(props.select);
watch(() => props.data, (newValue) => {
datadata.value = newValue;
});
watch(
() => props.data,
(newValue) => {
datadata.value = newValue;
},
);
watch(() => props.select, (newValue) => {
datadata.value = newValue;
});
watch(
() => props.select,
(newValue) => {
datadata.value = newValue;
},
);
emitter.on('reset', (val) => datadata.value = val);
emitter.on("reset", (val) => (datadata.value = val));
</script>

View File

@@ -1,9 +1,19 @@
<template>
<ul id="cyp-conformance-result-dot">
<li class="flex justify-start items-center py-1 pr-4" v-for="(act, index) in data" :key="index + act" :title="act">
<span class="material-symbols-outlined disc !text-sm align-middle mr-1">fiber_manual_record</span>
<li
class="flex justify-start items-center py-1 pr-4"
v-for="(act, index) in data"
:key="index + act"
:title="act"
>
<span class="material-symbols-outlined disc !text-sm align-middle mr-1"
>fiber_manual_record</span
>
<span class="mr-2 block w-12">{{ act.category }}</span>
<span class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden block">{{ act.task }}</span>
<span
class="px-2 py-1 border border-neutral-500 w-full whitespace-nowrap break-keep text-ellipsis overflow-hidden block"
>{{ act.task }}</span
>
</li>
</ul>
</template>
@@ -19,16 +29,20 @@
* and task pairs.
*/
import { ref, watch } from 'vue';
import emitter from '@/utils/emitter';
import { ref, watch } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(['timeResultData', 'select']);
const props = defineProps(["timeResultData", "select"]);
const data = ref(props.select);
watch(() => props.timeResultData, (newValue) => {
data.value = newValue;
}, { deep: true });
watch(
() => props.timeResultData,
(newValue) => {
data.value = newValue;
},
{ deep: true },
);
emitter.on('reset', (val) => data.value = val);
emitter.on("reset", (val) => (data.value = val));
</script>

View File

@@ -1,11 +1,23 @@
<template>
<div id="timeranges_s_e_container" class="flex justify-between items-center">
<Durationjs :max="minVuemax" :min="minVuemin" :size="'min'" :updateMax="updateMax"
@total-seconds="minTotalSeconds" :value="durationMin">
<Durationjs
:max="minVuemax"
:min="minVuemin"
:size="'min'"
:updateMax="updateMax"
@total-seconds="minTotalSeconds"
:value="durationMin"
>
</Durationjs>
<span>~</span>
<Durationjs :max="maxVuemax" :min="maxVuemin" :size="'max'" :updateMin="updateMin"
@total-seconds="maxTotalSeconds" :value="durationMax">
<span>~</span>
<Durationjs
:max="maxVuemax"
:min="maxVuemin"
:size="'max'"
:updateMin="updateMin"
@total-seconds="maxTotalSeconds"
:value="durationMax"
>
</Durationjs>
</div>
</template>
@@ -22,11 +34,11 @@
* for conformance time-based rules.
*/
import { ref, watch } from 'vue';
import Durationjs from '@/components/durationjs.vue';
import { ref, watch } from "vue";
import Durationjs from "@/components/durationjs.vue";
const props = defineProps(['time', 'select']);
const emit = defineEmits(['min-total-seconds', 'max-total-seconds']);
const props = defineProps(["time", "select"]);
const emit = defineEmits(["min-total-seconds", "max-total-seconds"]);
const timeData = ref({ min: 0, max: 0 });
const timeRangeMin = ref(0);
@@ -56,7 +68,7 @@ function setTimeValue() {
function minTotalSeconds(e) {
timeRangeMin.value = e;
updateMin.value = e;
emit('min-total-seconds', e);
emit("min-total-seconds", e);
}
/**
@@ -66,29 +78,33 @@ function minTotalSeconds(e) {
function maxTotalSeconds(e) {
timeRangeMax.value = e;
updateMax.value = e;
emit('max-total-seconds', e);
emit("max-total-seconds", e);
}
watch(() => props.time, (newValue, oldValue) => {
durationMax.value = null;
durationMin.value = null;
if(newValue === null) {
timeData.value = { min: 0, max: 0 };
}else if(newValue !== null) {
timeData.value = { min: newValue.min, max: newValue.max };
emit('min-total-seconds', newValue.min);
emit('max-total-seconds', newValue.max);
}
setTimeValue();
}, { deep: true, immediate: true });
watch(
() => props.time,
(newValue, oldValue) => {
durationMax.value = null;
durationMin.value = null;
if (newValue === null) {
timeData.value = { min: 0, max: 0 };
} else if (newValue !== null) {
timeData.value = { min: newValue.min, max: newValue.max };
emit("min-total-seconds", newValue.min);
emit("max-total-seconds", newValue.max);
}
setTimeValue();
},
{ deep: true, immediate: true },
);
// created
if(props.select){
if(Object.keys(props.select.base).length !== 0) {
if (props.select) {
if (Object.keys(props.select.base).length !== 0) {
timeData.value = props.select.base;
setTimeValue();
}
if(Object.keys(props.select.rule).length !== 0) {
if (Object.keys(props.select.rule).length !== 0) {
durationMin.value = props.select.rule.min;
durationMax.value = props.select.rule.max;
}

View File

@@ -1,65 +1,112 @@
<template>
<Dialog :visible="listModal" @update:visible="emit('closeModal', $event)" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass">
<template #header>
<div class=" py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
</div>
</template>
<div class="h-full flex items-start justify-start p-4">
<!-- Trace List -->
<section class="w-80 h-full pr-4">
<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">
<caption class="hidden">Trace List</caption>
<thead class="sticky top-0 z-10 bg-neutral-100">
<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)">
<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="progressWidth(trace.value)"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
</tr>
</tbody>
</table>
<Dialog
:visible="listModal"
@update:visible="emit('closeModal', $event)"
modal
:style="{ width: '90vw', height: '90vh' }"
:contentClass="contentClass"
>
<template #header>
<div class="py-5">
<p class="text-base font-bold">Non-conformance Issue</p>
</div>
</section>
<!-- Trace item Table -->
<section class="px-4 py-2 h-full w-[calc(100%_-_320px)] bg-neutral-10 rounded-xl">
<p class="h2 mb-2 px-4">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cfmTrace" ref="cfmTrace" class="h-full min-w-full relative"></div>
</template>
<div class="h-full flex items-start justify-start p-4">
<!-- Trace List -->
<section class="w-80 h-full pr-4">
<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">
<caption class="hidden">
Trace List
</caption>
<thead class="sticky top-0 z-10 bg-neutral-100">
<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)"
>
<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="progressWidth(trace.value)"
></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
</section>
<!-- Trace item Table -->
<section
class="px-4 py-2 h-full w-[calc(100%_-_320px)] bg-neutral-10 rounded-xl"
>
<p class="h2 mb-2 px-4">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div
id="cfmTrace"
ref="cfmTrace"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div
class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] 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">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
</div>
</DataTable>
</div>
</section>
</div>
</Dialog>
</div>
</section>
</div>
</Dialog>
</template>
<script setup>
// The Lucia project.
@@ -74,30 +121,39 @@
* results with expandable activity sequences.
*/
import { ref, computed, watch, nextTick, useTemplateRef } from 'vue';
import { storeToRefs } from 'pinia';
import { useConformanceStore } from '@/stores/conformance';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch, nextTick, useTemplateRef } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const props = defineProps(['listModal', 'listNo', 'traceId', 'firstCases', 'listTraces', 'taskSeq', 'cases', 'category']);
const emit = defineEmits(['closeModal']);
const props = defineProps([
"listModal",
"listNo",
"traceId",
"firstCases",
"listTraces",
"taskSeq",
"cases",
"category",
]);
const emit = defineEmits(["closeModal"]);
const conformanceStore = useConformanceStore();
const { infinite404 } = storeToRefs(conformanceStore);
// template ref
const cfmTrace = useTemplateRef('cfmTrace');
const cfmTrace = useTemplateRef("cfmTrace");
// data
const contentClass = ref('!bg-neutral-100 border-t border-neutral-300 h-full');
const contentClass = ref("!bg-neutral-100 border-t border-neutral-300 h-full");
const showTraceId = ref(null);
const infiniteData = ref(null);
const maxItems = ref(false);
const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const startNum = ref(0);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
// computed
@@ -106,23 +162,27 @@ const traceTotal = computed(() => {
});
const traceList = computed(() => {
const sum = props.listTraces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const sum = props.listTraces
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
return props.listTraces.map(trace => {
return {
id: trace.id,
value: Number((getPercentLabel(trace.count / sum))),
count: trace.count.toLocaleString('en-US'),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
return props.listTraces
.map((trace) => {
return {
id: trace.id,
value: Number(getPercentLabel(trace.count / sum)),
count: trace.count.toLocaleString("en-US"),
count_base: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
})
.sort((x, y) => x.id - y.id);
});
const caseData = computed(() => {
if(infiniteData.value !== null){
if (infiniteData.value !== null) {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // Create a new key-value pair
});
@@ -132,47 +192,74 @@ const caseData = computed(() => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
}
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
const facetName = facName => facName.trim().replace(/^(.)(.*)$/, (match, firstChar, restOfString) => firstChar.toUpperCase() + restOfString.toLowerCase());
const facetName = (facName) =>
facName
.trim()
.replace(
/^(.)(.*)$/,
(match, firstChar, restOfString) =>
firstChar.toUpperCase() + restOfString.toLowerCase(),
);
const result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
...data[0].facets.map((fac, index) => ({ field: `fac_${index}`, header: facetName(fac.name) })),
...data[0].attributes.map((att, index) => ({ field: `att_${index}`, header: att.key })),
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...data[0].facets.map((fac, index) => ({
field: `fac_${index}`,
header: facetName(fac.name),
})),
...data[0].attributes.map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
return result
return result;
});
// watch
watch(() => props.listModal, (newValue) => { // Draw the chart when the modal is opened for the first time
if(newValue) createCy();
});
watch(
() => props.listModal,
(newValue) => {
// Draw the chart when the modal is opened for the first time
if (newValue) createCy();
},
);
watch(() => props.taskSeq, (newValue) => {
if (newValue !== null) createCy();
});
watch(
() => props.taskSeq,
(newValue) => {
if (newValue !== null) createCy();
},
);
watch(() => props.traceId, (newValue) => {
// Update showTraceId when the traceId prop changes
showTraceId.value = newValue;
});
watch(
() => props.traceId,
(newValue) => {
// Update showTraceId when the traceId prop changes
showTraceId.value = newValue;
},
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(() => props.firstCases, (newValue) => {
infiniteData.value = newValue;
});
watch(
() => props.firstCases,
(newValue) => {
infiniteData.value = newValue;
},
);
watch(infinite404, (newValue) => {
if (newValue === 404) maxItems.value = true;
@@ -184,8 +271,8 @@ watch(infinite404, (newValue) => {
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
/**
@@ -193,78 +280,93 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* switch case data
* @param {number} id case id
*/
async function switchCaseData(id) {
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
infinite404.value = null;
maxItems.value = false;
startNum.value = 0;
let result;
if(props.category === 'issue') result = await conformanceStore.getConformanceTraceDetail(props.listNo, id, 0);
else if(props.category === 'loop') result = await conformanceStore.getConformanceLoopsTraceDetail(props.listNo, id, 0);
if (props.category === "issue")
result = await conformanceStore.getConformanceTraceDetail(
props.listNo,
id,
0,
);
else if (props.category === "loop")
result = await conformanceStore.getConformanceLoopsTraceDetail(
props.listNo,
id,
0,
);
infiniteData.value = await result;
showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
if(props.taskSeq !== null) {
if (props.taskSeq !== null) {
props.taskSeq.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
});
};
}
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
if(props.taskSeq !== null) {
if (props.taskSeq !== null) {
props.taskSeq.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
};
}
// The number of edges is one less than the number of nodes
processMap.value.edges.pop();
}
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
nextTick(() => {
const graphId = cfmTrace.value;
setNodesData();
setEdgesData();
if(graphId !== null) cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
if (graphId !== null)
cytoscapeMapTrace(
processMap.value.nodes,
processMap.value.edges,
graphId,
);
});
}
/**
@@ -273,12 +375,16 @@ function createCy(){
async function fetchData() {
try {
infiniteFinish.value = false;
startNum.value += 20
const result = await conformanceStore.getConformanceTraceDetail(props.listNo, showTraceId.value, startNum.value);
startNum.value += 20;
const result = await conformanceStore.getConformanceTraceDetail(
props.listNo,
showTraceId.value,
startNum.value,
);
infiniteData.value = [...infiniteData.value, ...result];
infiniteFinish.value = true;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
/**
@@ -286,10 +392,16 @@ async function fetchData() {
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(maxItems.value || infiniteData.value.length < 20 || infiniteFinish.value === false) return;
if (
maxItems.value ||
infiniteData.value.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
@@ -298,22 +410,22 @@ function handleScroll(event) {
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@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;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
/* Center header title */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;

View File

@@ -1,30 +1,70 @@
<template>
<!-- Activity List -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<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 text-sm" :class="data.length === 0? 'h-full': null">
<caption class="hidden">Activity List</caption>
<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 text-sm"
:class="data.length === 0 ? 'h-full' : null"
>
<caption class="hidden">
Activity List
</caption>
<thead class="sticky top-0 left-0 z-10 bg-neutral-10">
<tr>
<th class="text-start font-semibold leading-10 px-2 border-b border-neutral-500">Activity</th>
<th class="font-semibold leading-10 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
<th
class="text-start font-semibold leading-10 px-2 border-b border-neutral-500"
>
Activity
</th>
<th
class="font-semibold leading-10 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<Draggable :list="data" :group="{ name: 'activity', pull: 'clone', put: false }" itemKey="name" tag="tbody" animation="300" @end="onEnd" :fallbackTolerance="5" :forceFallback="true" :ghostClass="'ghostSelected'" :dragClass="'dragSelected'" :sort="false">
<Draggable
:list="data"
:group="{ name: 'activity', pull: 'clone', put: false }"
itemKey="name"
tag="tbody"
animation="300"
@end="onEnd"
:fallbackTolerance="5"
:forceFallback="true"
:ghostClass="'ghostSelected'"
:dragClass="'dragSelected'"
:sort="false"
>
<template #item="{ element, index }">
<tr @dblclick="moveActItem(index, element)" :class="listSequence.includes(element) ? 'text-primary' : ''">
<tr
@dblclick="moveActItem(index, element)"
:class="listSequence.includes(element) ? 'text-primary' : ''"
>
<td class="px-4 py-2" :id="element.label">{{ 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
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>
<td class="px-4 py-2 text-right">
{{ element.occurrence_ratio }}
</td>
</tr>
</template>
</Draggable>
@@ -32,24 +72,61 @@
</div>
</div>
<!-- Sequence -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm">
<p class="h2 border-b border-500 my-2">Sequence&nbsp({{ listSeq.length }})</p>
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm"
>
<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 at least two activities here and sort.</p>
<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 at least two activities 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 listSequence">
<draggable class="h-full" :list="listSequence" :group="{name: 'activity'}" itemKey="name" animation="300" :forceFallback="true" :fallbackTolerance="5" :dragClass="'!opacity-100'" @start="onStart" @end="onEnd" :component-data="getComponentData()" :ghostClass="'!opacity-0'">
<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 listSequence"
>
<draggable
class="h-full"
:list="listSequence"
:group="{ name: 'activity' }"
itemKey="name"
animation="300"
:forceFallback="true"
:fallbackTolerance="5"
:dragClass="'!opacity-100'"
@start="onStart"
@end="onEnd"
:component-data="getComponentData()"
:ghostClass="'!opacity-0'"
>
<template #item="{ element, index }">
<div>
<div class="flex justify-center items-center">
<div class="w-full p-2 border border-primary rounded text-primary bg-neutral-10" @dblclick="moveSeqItem(index, element)"><span>{{ element.label }}</span>
<div
class="w-full p-2 border border-primary rounded text-primary bg-neutral-10"
@dblclick="moveSeqItem(index, element)"
>
<span>{{ element.label }}</span>
</div>
<span class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger" @click.stop.native="moveSeqItem(index, element)">close</span>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop.native="moveSeqItem(index, element)"
>close</span
>
</div>
<span v-show="index !== listSeq.length - 1 && index !== lastItemIndex - 1" class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"></span>
<span
v-show="
index !== listSeq.length - 1 && index !== lastItemIndex - 1
"
class="pi pi-chevron-down !text-lg inline-block py-2 pr-7"
></span>
</div>
</template>
</draggable>
@@ -68,8 +145,8 @@
* rules.
*/
import { ref, computed, watch } from 'vue';
import { sortNumEngZhtwForFilter } from '@/module/sortNumEngZhtw.js';
import { ref, computed, watch } from "vue";
import { sortNumEngZhtwForFilter } from "@/module/sortNumEngZhtw.js";
const props = defineProps({
filterTaskData: {
@@ -83,10 +160,10 @@ const props = defineProps({
listSeq: {
type: Array,
required: true,
}
},
});
const emit = defineEmits(['update:listSeq']);
const emit = defineEmits(["update:listSeq"]);
const listSequence = ref([]);
const filteredData = ref(props.filterTaskData);
@@ -101,13 +178,19 @@ const data = computed(() => {
return filteredData.value;
});
watch(() => props.listSeq, (newval) => {
listSequence.value = newval;
});
watch(
() => props.listSeq,
(newval) => {
listSequence.value = newval;
},
);
watch(() => props.filterTaskData, (newval) => {
filteredData.value = newval;
});
watch(
() => props.filterTaskData,
(newval) => {
filteredData.value = newval;
},
);
/**
* Moves an activity from the list to the sequence on double-click.
@@ -129,7 +212,7 @@ function moveSeqItem(index, element) {
/** Emits the current sequence list to the parent component. */
function getComponentData() {
emit('update:listSeq', listSequence.value);
emit("update:listSeq", listSequence.value);
}
/**
@@ -138,13 +221,13 @@ function getComponentData() {
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = 'none';
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
originalElement.style.display = 'none';
originalElement.style.display = "none";
// When dragging the last element, hide the arrow of the second-to-last element
const listIndex = listSequence.value.length - 1;
if(evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
if (evt.oldIndex === listIndex) lastItemIndex.value = listIndex;
}
/**
@@ -154,12 +237,12 @@ function onStart(evt) {
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
originalElement.style.display = '';
originalElement.style.display = "";
// Show the arrow after drag ends, except for the last element
const lastChild = evt.item.lastChild;
const listIndex = listSequence.value.length - 1
const listIndex = listSequence.value.length - 1;
if (evt.oldIndex !== listIndex) {
lastChild.style.display = '';
lastChild.style.display = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
@@ -168,9 +251,9 @@ function onEnd(evt) {
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@apply shadow-[0px_0px_100px_-10px_inset] shadow-neutral-200
@apply shadow-[0px_0px_100px_-10px_inset] shadow-neutral-200;
}
.dragSelected {
@apply shadow-[0px_0px_4px_2px] bg-neutral-10 shadow-neutral-300 !opacity-100
@apply shadow-[0px_0px_4px_2px] bg-neutral-10 shadow-neutral-300 !opacity-100;
}
</style>

View File

@@ -1,29 +1,69 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<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>
</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" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect">
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<DataTable
v-model:selection="select"
:value="tableData"
dataKey="label"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@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="occurrences_base" 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
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="occurrences_base"
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
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="Progress" 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
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>
<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>
@@ -38,7 +78,7 @@
* occurrences filter table with single-row radio selection.
*/
import { ref, watch } from 'vue';
import { ref, watch } from "vue";
const props = defineProps({
tableTitle: {
@@ -51,28 +91,31 @@ const props = defineProps({
},
tableSelect: {
type: [Object, Array],
default: null
default: null,
},
progressWidth: {
type: Function,
required: false,
}
},
});
const emit = defineEmits(['on-row-select']);
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const metaKey = ref(true);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
watch(
() => props.tableSelect,
(newval) => {
select.value = newval;
},
);
/**
* Emits the selected row to the parent component.
* @param {Event} e - The row selection event.
*/
function onRowSelect(e) {
emit('on-row-select', e);
emit("on-row-select", e);
}
</script>

View File

@@ -4,36 +4,93 @@
<p class="h2">{{ tableTitle }}&nbsp({{ data.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="data" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll" @row-unselect-all="onRowUnelectAll">
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<DataTable
v-model:selection="select"
:value="data"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@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 allCheckboxAct"></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="occurrences_base" 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="cases_base" 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" />
<Column
selectionMode="multiple"
headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10 allCheckboxAct"
></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="occurrences_base"
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="cases_base"
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
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="Progress" 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
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
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="Progress" 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
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>
<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>
@@ -50,27 +107,35 @@
* selection.
*/
import { ref, watch } from 'vue';
import { ref, watch } from "vue";
const props = defineProps(['tableTitle', 'tableData', 'tableSelect', 'progressWidth']);
const props = defineProps([
"tableTitle",
"tableData",
"tableSelect",
"progressWidth",
]);
const emit = defineEmits(['on-row-select']);
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const data = ref(props.tableData);
watch(() => props.tableSelect, (newval) => {
select.value = newval;
});
watch(
() => props.tableSelect,
(newval) => {
select.value = newval;
},
);
/** Emits the current selection when a row is selected. */
function onRowSelect() {
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/** Emits the current selection when a row is unselected. */
function onRowUnselect() {
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/**
@@ -79,7 +144,7 @@ function onRowUnselect() {
*/
function onRowSelectAll(e) {
select.value = e.data;
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
/**
@@ -88,6 +153,6 @@ function onRowSelectAll(e) {
*/
function onRowUnelectAll(e) {
select.value = null;
emit('on-row-select', select.value);
emit("on-row-select", select.value);
}
</script>

View File

@@ -1,123 +1,310 @@
<template>
<section class="w-full h-full">
<p class="h2 ml-1 mb-2">Activity Select</p>
<div class="flex flex-row justify-between items-start gap-4 w-full h-[calc(100%_-_48px)]">
<!-- Attribute Name -->
<div class="basis-1/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm">
<p class="h2 my-2">Attribute Name ({{ attTotal }})<span class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer" v-tooltip.bottom="tooltip.attributeName">info</span></p>
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_56px)]">
<DataTable v-model:selection="selectedAttName" :value="filterAttrs" dataKey="key" breakpoint="0" :tableClass="tableClass" @row-select="switchAttNameRadio">
<Column selectionMode="single" :headerClass="headerModeClass" :bodyClass="bodyModeClass"></Column>
<Column field="key" header="Attribute" :headerClass="headerClass" :bodyClass="bodyClass" sortable>
<template #body="slotProps">
<div :title="slotProps.data.key" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
<section class="w-full h-full">
<p class="h2 ml-1 mb-2">Activity Select</p>
<div
class="flex flex-row justify-between items-start gap-4 w-full h-[calc(100%_-_48px)]"
>
<!-- Attribute Name -->
<div
class="basis-1/3 bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm"
>
<p class="h2 my-2">
Attribute Name ({{ attTotal }})<span
class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer"
v-tooltip.bottom="tooltip.attributeName"
>info</span
>
</p>
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_56px)]"
>
<DataTable
v-model:selection="selectedAttName"
:value="filterAttrs"
dataKey="key"
breakpoint="0"
:tableClass="tableClass"
@row-select="switchAttNameRadio"
>
<Column
selectionMode="single"
:headerClass="headerModeClass"
:bodyClass="bodyModeClass"
></Column>
<Column
field="key"
header="Attribute"
:headerClass="headerClass"
:bodyClass="bodyClass"
sortable
>
<template #body="slotProps">
<div
:title="slotProps.data.key"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.key }}
</div>
</template>
</Column>
</DataTable>
</div>
</div>
<!-- Range Selection -->
<div class="basis-2/3 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">Range Selection {{ attRangeTotal }}</p>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 w-full h-[calc(100%_-_70px)]">
<!-- type: boolean -->
<div v-if="selectedAttName.type === 'boolean'" class="w-full">
<DataTable v-model:selection="selectedAttRange" :value="attRangeData" dataKey="id" breakpoint="0" :tableClass="tableClass" @row-select="onRowSelect" >
<ColumnGroup type="header">
<Row>
<Column selectionMode="single" :headerClass="headerModeClass" ></Column>
<Column field="value" header="Value" :headerClass="headerClass" sortable />
<Column field="freq" header="Occurrences" :headerClass="headerClass" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="single" :bodyClass="bodyModeClass"></Column>
<Column field="label" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div :title="slotProps.data.label" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column header="Progress" 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_progress_bar)"></div>
</div>
</template>
</Column>
<Column field="occ_value" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occ_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
<!-- type: string -->
<div v-else-if="selectedAttName.type === 'string'" class="w-full">
<DataTable v-model:selection="selectedAttRange" :value="attRangeData" dataKey="id" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll($event)" @row-unselect-all="onRowUnelectAll">
<ColumnGroup type="header">
<Row>
<Column selectionMode="multiple" :headerClass="headerModeClass" ></Column>
<Column field="value" header="Value" :headerClass="headerClass" sortable />
<Column field="freq" header="Occurrences" :headerClass="headerClass" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="multiple" :bodyClass="bodyModeClass"></Column>
<Column field="value" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div :title="slotProps.data.value" class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full">
{{ slotProps.data.value }}
</div>
</template>
</Column>
<Column header="Progress" 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_progress_bar)"></div>
</div>
</template>
</Column>
<Column field="occ_value" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occ_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
<!-- Range Selection -->
<div
class="basis-2/3 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">Range Selection {{ attRangeTotal }}</p>
</div>
<!-- type: value -->
<div v-else-if="valueTypes.includes(selectedAttName.type)" class="space-y-2 text-sm w-full">
<!-- Chart.js -->
<div class="h-3/5 relative">
<Chart type="line" :data="chartData" :options="chartOptions" class="h-30rem" id="chartCanvasId"/>
<div id="chart-mask-left" class="absolute bg-neutral-10/50"></div>
<div id="chart-mask-right" class="absolute bg-neutral-10/50"></div>
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 w-full h-[calc(100%_-_70px)]"
>
<!-- type: boolean -->
<div v-if="selectedAttName.type === 'boolean'" class="w-full">
<DataTable
v-model:selection="selectedAttRange"
:value="attRangeData"
dataKey="id"
breakpoint="0"
:tableClass="tableClass"
@row-select="onRowSelect"
>
<ColumnGroup type="header">
<Row>
<Column
selectionMode="single"
:headerClass="headerModeClass"
></Column>
<Column
field="value"
header="Value"
:headerClass="headerClass"
sortable
/>
<Column
field="freq"
header="Occurrences"
:headerClass="headerClass"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column
selectionMode="single"
:bodyClass="bodyModeClass"
></Column>
<Column field="label" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div
:title="slotProps.data.label"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.label }}
</div>
</template>
</Column>
<Column
header="Progress"
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_progress_bar)"
></div>
</div>
</template>
</Column>
<Column
field="occ_value"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occ_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
<!-- Slider -->
<div class="px-2 py-3">
<Slider v-model="selectArea" :step="1" :min="0" :max="selectRange" range class="mx-2" @change="changeSelectArea($event)"/>
<!-- type: string -->
<div v-else-if="selectedAttName.type === 'string'" class="w-full">
<DataTable
v-model:selection="selectedAttRange"
:value="attRangeData"
dataKey="id"
breakpoint="0"
tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm"
@row-select="onRowSelect"
@row-unselect="onRowUnselect"
@row-select-all="onRowSelectAll($event)"
@row-unselect-all="onRowUnelectAll"
>
<ColumnGroup type="header">
<Row>
<Column
selectionMode="multiple"
:headerClass="headerModeClass"
></Column>
<Column
field="value"
header="Value"
:headerClass="headerClass"
sortable
/>
<Column
field="freq"
header="Occurrences"
:headerClass="headerClass"
sortable
:colspan="3"
/>
</Row>
</ColumnGroup>
<Column
selectionMode="multiple"
:bodyClass="bodyModeClass"
></Column>
<Column field="value" header="Activity" :bodyClass="bodyClass">
<template #body="slotProps">
<div
:title="slotProps.data.value"
class="whitespace-nowrap break-keep overflow-hidden text-ellipsis w-full"
>
{{ slotProps.data.value }}
</div>
</template>
</Column>
<Column
header="Progress"
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_progress_bar)"
></div>
</div>
</template>
</Column>
<Column
field="occ_value"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occ_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
<!-- Calendar / InputNumber group -->
<div>
<div v-if="selectedAttName.type === 'date'" class="flex justify-center items-center space-x-2 w-full">
<div>
<span class="block mb-2">Start time</span>
<Calendar v-model="startTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="startMinDate" :maxDate="startMaxDate" showTime showIcon hourFormat="24" @date-select="sliderValueRange($event, 'start')" id="startCalendar" />
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar v-model="endTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="endMinDate" :maxDate="endMaxDate" showTime showIcon hourFormat="24" @date-select="sliderValueRange($event, 'end')" id="endCalendar"/>
</div>
<!-- type: value -->
<div
v-else-if="valueTypes.includes(selectedAttName.type)"
class="space-y-2 text-sm w-full"
>
<!-- Chart.js -->
<div class="h-3/5 relative">
<Chart
type="line"
:data="chartData"
:options="chartOptions"
class="h-30rem"
id="chartCanvasId"
/>
<div id="chart-mask-left" class="absolute bg-neutral-10/50"></div>
<div
id="chart-mask-right"
class="absolute bg-neutral-10/50"
></div>
</div>
<div v-else class="flex justify-center items-center space-x-2 w-full">
<InputNumber v-model="valueStart" :min="valueStartMin" :max="valueStartMax" :maxFractionDigits="2" inputClass="w-24 text-sm text-right" @blur="sliderValueRange($event, 'start')"></InputNumber>
<span class="block px-2">~</span>
<InputNumber v-model="valueEnd" :min="valueEndMin" :max="valueEndMax" inputClass="w-24 text-sm text-right" :maxFractionDigits="2" @blur="sliderValueRange($event, 'end')"></InputNumber>
<!-- Slider -->
<div class="px-2 py-3">
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="selectRange"
range
class="mx-2"
@change="changeSelectArea($event)"
/>
</div>
<!-- Calendar / InputNumber group -->
<div>
<div
v-if="selectedAttName.type === 'date'"
class="flex justify-center items-center space-x-2 w-full"
>
<div>
<span class="block mb-2">Start time</span>
<Calendar
v-model="startTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="startMinDate"
:maxDate="startMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderValueRange($event, 'start')"
id="startCalendar"
/>
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar
v-model="endTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="endMinDate"
:maxDate="endMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderValueRange($event, 'end')"
id="endCalendar"
/>
</div>
</div>
<div
v-else
class="flex justify-center items-center space-x-2 w-full"
>
<InputNumber
v-model="valueStart"
:min="valueStartMin"
:max="valueStartMax"
:maxFractionDigits="2"
inputClass="w-24 text-sm text-right"
@blur="sliderValueRange($event, 'start')"
></InputNumber>
<span class="block px-2">~</span>
<InputNumber
v-model="valueEnd"
:min="valueEndMin"
:max="valueEndMax"
inputClass="w-24 text-sm text-right"
:maxFractionDigits="2"
@blur="sliderValueRange($event, 'end')"
></InputNumber>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</section>
</template>
<script setup>
// The Lucia project.
@@ -132,24 +319,24 @@
* for filtering by attribute values.
*/
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { setLineChartData } from '@/module/setChartData.js';
import getMoment from 'moment';
import InputNumber from 'primevue/inputnumber';
import { Decimal } from 'decimal.js';
import emitter from '@/utils/emitter';
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { setLineChartData } from "@/module/setChartData.js";
import getMoment from "moment";
import InputNumber from "primevue/inputnumber";
import { Decimal } from "decimal.js";
import emitter from "@/utils/emitter";
const emit = defineEmits(['select-attribute']);
const emit = defineEmits(["select-attribute"]);
const allMapDataStore = useAllMapDataStore();
const { filterAttrs } = storeToRefs(allMapDataStore);
const selectedAttName = ref({});
const selectedAttRange = ref(null);
const valueTypes = ['int', 'float', 'date'];
const classTypes = ['boolean', 'string'];
const valueTypes = ["int", "float", "date"];
const classTypes = ["boolean", "string"];
const chartData = ref({});
const chartOptions = ref({});
const chartComplete = ref(null); // Rendered chart.js instance data
@@ -162,16 +349,19 @@ const startMaxDate = ref(null);
const endMinDate = ref(null);
const endMaxDate = ref(null);
const valueStart = ref(null); // PrimeVue InputNumber v-model
const valueEnd = ref(null); // PrimeVue InputNumber v-model
const valueEnd = ref(null); // PrimeVue InputNumber v-model
const valueStartMin = ref(null);
const valueStartMax = ref(null);
const valueEndMin = ref(null);
const valueEndMax = ref(null);
const tableClass = 'w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm';
const headerModeClass = 'w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10';
const headerClass = '!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10';
const bodyModeClass = '!p-2 !border-0';
const bodyClass = 'break-words !py-2 !border-0';
const tableClass =
"w-full h-full !border-separate !border-spacing-x-2 !table-auto text-sm";
const headerModeClass =
"w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10";
const headerClass =
"!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10";
const bodyModeClass = "!p-2 !border-0";
const bodyClass = "break-words !py-2 !border-0";
const panelProps = {
onClick: (event) => {
event.stopPropagation();
@@ -179,8 +369,9 @@ const panelProps = {
};
const tooltip = {
attributeName: {
value: 'Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.',
class: '!max-w-[212px] !text-[10px] !opacity-90',
value:
"Attributes with too many discrete values are excluded from selection. But users can still view those attributes in the DATA page.",
class: "!max-w-[212px] !text-[10px] !opacity-90",
},
};
@@ -194,7 +385,7 @@ const attRangeTotal = computed(() => {
let result = null; // Initialize the result variable with null
if (classTypes.includes(type) && attRangeData.value) {
result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
}
return result;
});
@@ -202,7 +393,9 @@ const attRangeTotal = computed(() => {
const attRangeData = computed(() => {
let data = [];
const type = selectedAttName.value.type;
const sum = selectedAttName.value.options.map(item => item.freq).reduce((acc, cur) => acc + cur, 0);
const sum = selectedAttName.value.options
.map((item) => item.freq)
.reduce((acc, cur) => acc + cur, 0);
data = selectedAttName.value.options.map((item, index) => {
const ratio = item.freq / sum;
const result = {
@@ -211,27 +404,31 @@ const attRangeData = computed(() => {
type: type,
value: item.value,
occ_progress_bar: ratio * 100,
occ_value: item.freq.toLocaleString('en-US'),
occ_value: item.freq.toLocaleString("en-US"),
occ_ratio: getPercentLabel(ratio),
freq: item.freq
freq: item.freq,
};
result.label = null;
if (type === 'boolean') {
result.label = item.value ? 'Yes' : 'No';
if (type === "boolean") {
result.label = item.value ? "Yes" : "No";
} else {
result.label = null;
}
return result;
})
});
return data.sort((x, y) => y.freq - x.freq);
});
// Get the selected Attribute radio's numeric-type data
const valueData = computed(() => {
// filter returns an array, find returns the first matched element, so use find here.
if(valueTypes.includes(selectedAttName.value.type)){
const data = filterAttrs.value.find(item => item.type === selectedAttName.value.type && item.key === selectedAttName.value.key);
return data
if (valueTypes.includes(selectedAttName.value.type)) {
const data = filterAttrs.value.find(
(item) =>
item.type === selectedAttName.value.type &&
item.key === selectedAttName.value.key,
);
return data;
}
});
@@ -243,8 +440,8 @@ const sliderDataComputed = computed(() => {
const max = valueData.value.max;
const type = valueData.value.type;
switch (type) {
case 'dummy':
case 'date':
case "dummy":
case "date":
xAxisMin = new Date(min).getTime();
xAxisMax = new Date(max).getTime();
break;
@@ -255,25 +452,25 @@ const sliderDataComputed = computed(() => {
}
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
let data = []
let data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
data.push(xAxisMin + step * i);
}
switch (type) {
case 'int':
data = data.map(value => {
case "int":
data = data.map((value) => {
let result = Math.round(value);
result = result === -0 ? 0 : result;
return result;
});
break;
case 'float':
data = data.map(value => {
case "float":
data = data.map((value) => {
let result = new Decimal(value.toFixed(2)).toNumber();
result = result === -0 ? 0 : result;
return result;
})
});
break;
default:
break;
@@ -288,25 +485,26 @@ const attValueTypeStartEnd = computed(() => {
const type = selectedAttName.value.type;
switch (type) {
case 'dummy': //sonar-qube
case 'date':
start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
case "dummy": //sonar-qube
case "date":
start = getMoment(startTime.value).format("YYYY-MM-DDTHH:mm:00");
end = getMoment(endTime.value).format("YYYY-MM-DDTHH:mm:00");
break;
default:
start = valueStart.value;
end = valueEnd.value;
break;
}
const data = { // Data to send to the backend
const data = {
// Data to send to the backend
type: type,
data: {
key: selectedAttName.value.key,
min: start,
max: end,
}
}
emit('select-attribute', data);
},
};
emit("select-attribute", data);
return [start, end];
});
@@ -317,7 +515,7 @@ const labelsData = computed(() => {
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
@@ -333,7 +531,7 @@ function onRowSelect() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -345,7 +543,7 @@ function onRowUnselect() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -359,7 +557,7 @@ function onRowSelectAll(e) {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data);
emit("select-attribute", data);
}
/**
@@ -372,7 +570,7 @@ function onRowUnelectAll() {
type: type,
data: selectedAttRange.value,
};
emit('select-attribute', data)
emit("select-attribute", data);
}
/**
@@ -385,14 +583,15 @@ function switchAttNameRadio(e) {
endTime.value = null;
valueStart.value = null;
valueEnd.value = null;
if(valueData.value) { // Switch Attribute Name
if (valueData.value) {
// Switch Attribute Name
// Initialize two-way bindings
selectArea.value = [0, selectRange.value];
const min = valueData.value.min;
const max = valueData.value.max;
switch (selectedAttName.value.type) {
case 'dummy': //sonar-qube
case 'date':
case "dummy": //sonar-qube
case "date":
// Clear two-way bindings except for date
valueStart.value = null;
valueEnd.value = null;
@@ -431,8 +630,8 @@ function switchAttNameRadio(e) {
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -440,8 +639,8 @@ function progressWidth(value){
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
@@ -461,7 +660,7 @@ function resizeMask(chart) {
* @param {object} chart - The Chart.js instance data.
*/
function resizeLeftMask(chart, from) {
const canvas = document.querySelector('#chartCanvasId canvas');
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`;
@@ -474,7 +673,7 @@ function resizeLeftMask(chart, from) {
* @param {object} chart - The Chart.js instance data.
*/
function resizeRightMask(chart, to) {
const canvas = document.querySelector('#chartCanvasId canvas');
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
@@ -488,33 +687,37 @@ function resizeRightMask(chart, to) {
function createChart() {
const vData = valueData.value;
const max = vData.chart.y_axis.max * 1.1;
const data = setLineChartData(vData.chart.data, vData.chart.x_axis.max, vData.chart.x_axis.min);
const isDateType = vData.type === 'date';
const data = setLineChartData(
vData.chart.data,
vData.chart.x_axis.max,
vData.chart.x_axis.min,
);
const isDateType = vData.type === "date";
const minX = vData.chart.x_axis.min;
const maxX = vData.chart.x_axis.max;
let setChartData= {};
let setChartOptions= {};
let setChartData = {};
let setChartOptions = {};
let setLabels = [];
switch (vData.type) {
case 'int':
setLabels = data.map(item => Math.round(item.x));
case "int":
setLabels = data.map((item) => Math.round(item.x));
break;
case 'float':
case "float":
setLabels = data.map((item, index) => {
let x;
if (index === 0) {
x = Math.floor(item.x * 100) / 100;
x = Math.floor(item.x * 100) / 100;
} else if (index === data.length - 1) {
item.x = Math.ceil(item.x * 100) / 100;
x = item.x;
item.x = Math.ceil(item.x * 100) / 100;
x = item.x;
} else {
x = Math.round(item.x * 100) / 100;
x = Math.round(item.x * 100) / 100;
}
return x
return x;
});
break;
case 'date':
case "date":
setLabels = labelsData.value;
break;
default:
@@ -523,14 +726,14 @@ function createChart() {
setChartData = {
datasets: [
{
label: 'Attribute Value',
label: "Attribute Value",
data: data,
fill: 'start',
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
}
},
],
labels: setLabels,
};
@@ -542,20 +745,20 @@ function createChart() {
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false
propagate: false,
},
title: false
title: false,
},
animation: {
onComplete: e => {
onComplete: (e) => {
chartComplete.value = e.chart;
resizeMask(e.chart);
}
},
},
interaction: {
intersect: true,
@@ -564,59 +767,60 @@ function createChart() {
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
},
},
},
};
if(isDateType) {
if (isDateType) {
setChartOptions.scales.x = {
type: 'time',
type: "time",
ticks: {
min: minX,
max: maxX,
autoSkip: true, // Automatically determine whether to convert time units
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
color: "#334155",
display: true,
source: 'labels', // Flexibly display label count proportionally
source: "labels", // Flexibly display label count proportionally
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
minUnit: 'day', // Minimum display unit
minUnit: "day", // Minimum display unit
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
}
},
};
} else {
setChartOptions.scales.x = {
bounds: 'data',
type: 'linear',
bounds: "data",
type: "linear",
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
callback: ((value, index, values) => {
color: "#334155",
callback: (value, index, values) => {
let x;
switch (vData.type) {
case 'int':
case "int":
return Math.round(value);
case 'float':
case "float":
switch (index) {
case 0:
x = Math.floor(value * 100) / 100;
@@ -629,15 +833,17 @@ function createChart() {
}
// Handle scientific notation and other format conversions
// Decimal cannot handle numbers exceeding 16 digits
x = new Intl.NumberFormat(undefined, {useGrouping: false}).format(x);
return x
x = new Intl.NumberFormat(undefined, {
useGrouping: false,
}).format(x);
return x;
}
})
},
},
grid: {
display: false, // Hide x-axis grid lines
},
}
};
}
chartData.value = setChartData;
chartOptions.value = setChartOptions;
@@ -654,8 +860,8 @@ function changeSelectArea(e) {
const end = sliderData[e[1].toFixed()]; // Get the index, which must be an integer.
switch (selectedAttName.value.type) {
case 'dummy':
case 'date':
case "dummy":
case "date":
startTime.value = new Date(start);
endTime.value = new Date(end);
// Reset the start/end calendar selection range
@@ -684,48 +890,56 @@ function changeSelectArea(e) {
function sliderValueRange(e, direction) {
// Find the closest index; time format: millisecond timestamps
const sliderData = sliderDataComputed.value;
const isDateType = selectedAttName.value.type === 'date';
const isDateType = selectedAttName.value.type === "date";
let targetTime = [];
let inputValue;
if(isDateType) targetTime = [new Date(attValueTypeStartEnd.value[0]).getTime(), new Date(attValueTypeStartEnd.value[1]).getTime()];
else targetTime = [attValueTypeStartEnd.value[0], attValueTypeStartEnd.value[1]]
if (isDateType)
targetTime = [
new Date(attValueTypeStartEnd.value[0]).getTime(),
new Date(attValueTypeStartEnd.value[1]).getTime(),
];
else
targetTime = [attValueTypeStartEnd.value[0], attValueTypeStartEnd.value[1]];
const closestIndexes = targetTime.map(target => {
const closestIndexes = targetTime.map((target) => {
let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
closestIndex =
((target - sliderData[0]) /
(sliderData[sliderData.length - 1] - sliderData[0])) *
sliderData.length;
let result = Math.round(Math.abs(closestIndex));
result = result > selectRange.value ? selectRange.value : result;
return result
return result;
});
// Update the slider
selectArea.value = closestIndexes;
// Reset the start/end calendar selection range
if(!isDateType) inputValue = Number(e.value.replace(/,/g, '')) ;
if(direction === 'start') {
if(isDateType){
if (!isDateType) inputValue = Number(e.value.replace(/,/g, ""));
if (direction === "start") {
if (isDateType) {
endMinDate.value = e;
} else {
valueEndMin.value = inputValue;
}
}
else if(direction === 'end') {
if(isDateType) {
} else if (direction === "end") {
if (isDateType) {
startMaxDate.value = e;
} else {
valueStartMax.value = inputValue;
};
}
}
// Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chartComplete.value);
if (!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1]))
resizeMask(chartComplete.value);
else return;
}
// created() equivalent
emitter.on('map-filter-reset', value => {
if(value) {
emitter.on("map-filter-reset", (value) => {
if (value) {
selectedAttRange.value = null;
if(valueData.value && valueTypes.includes(selectedAttName.value.type)){
if (valueData.value && valueTypes.includes(selectedAttName.value.type)) {
const min = valueData.value.min;
const max = valueData.value.max;
startTime.value = new Date(min);
@@ -745,12 +959,12 @@ onMounted(() => {
onBeforeUnmount(() => {
selectedAttName.value = {};
emitter.off('map-filter-reset');
emitter.off("map-filter-reset");
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
:deep(table tbody td:nth-child(2)) {
@apply whitespace-nowrap break-keep overflow-hidden text-ellipsis max-w-0
@apply whitespace-nowrap break-keep overflow-hidden text-ellipsis max-w-0;
}
</style>

View File

@@ -1,41 +1,75 @@
<template>
<div class=" w-full h-full">
<div class="h-[calc(100%_-_58px)] border-b border-neutral-400 mb-2 scrollbar overflow-x-hidden overflow-y-auto">
<div v-if="this.temporaryData.length === 0" class="h-full flex justify-center items-center">
<span class="text-neutral-500">No Filter.</span>
</div>
<div v-else>
<div class="text-primary h2 flex items-center justify-start my-4">
<span class="material-symbols-outlined m-2">info</span>
<p>Disabled filters will not be saved.</p>
</div>
<Timeline :value="ruleData">
<template #content="rule">
<div class="border-b border-neutral-300 flex justify-between items-center space-x-2">
<!-- content -->
<div class="pl-2 mb-2">
<p class="text-sm font-medium leading-5">{{ rule.item.type }}:&nbsp;<span class="text-neutral-500">{{ rule.item.label }}</span></p>
</div>
<!-- button -->
<div class="min-w-fit">
<InputSwitch v-model="rule.item.toggle" @input="isRule($event, rule.index)"/>
<button type="button" class="m-2 focus:ring focus:ring-danger/20 text-neutral-500 hover:text-danger" @click.stop="deleteRule(rule.index)">
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</template>
</Timeline>
</div>
<div class="w-full h-full">
<div
class="h-[calc(100%_-_58px)] border-b border-neutral-400 mb-2 scrollbar overflow-x-hidden overflow-y-auto"
>
<div
v-if="this.temporaryData.length === 0"
class="h-full flex justify-center items-center"
>
<span class="text-neutral-500">No Filter.</span>
</div>
<!-- Button -->
<div>
<div class="float-right space-x-4 px-4 py-2">
<button type="button" class="btn btn-sm " :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="deleteRule('all')">Delete All</button>
<button type="button" class="btn btn-sm" :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="submitAll">Apply All</button>
<div v-else>
<div class="text-primary h2 flex items-center justify-start my-4">
<span class="material-symbols-outlined m-2">info</span>
<p>Disabled filters will not be saved.</p>
</div>
<Timeline :value="ruleData">
<template #content="rule">
<div
class="border-b border-neutral-300 flex justify-between items-center space-x-2"
>
<!-- content -->
<div class="pl-2 mb-2">
<p class="text-sm font-medium leading-5">
{{ rule.item.type }}:&nbsp;<span class="text-neutral-500">{{
rule.item.label
}}</span>
</p>
</div>
<!-- button -->
<div class="min-w-fit">
<InputSwitch
v-model="rule.item.toggle"
@input="isRule($event, rule.index)"
/>
<button
type="button"
class="m-2 focus:ring focus:ring-danger/20 text-neutral-500 hover:text-danger"
@click.stop="deleteRule(rule.index)"
>
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</template>
</Timeline>
</div>
</div>
<!-- Button -->
<div>
<div class="float-right space-x-4 px-4 py-2">
<button
type="button"
class="btn btn-sm"
:class="[temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']"
:disabled="temporaryData.length === 0"
@click="deleteRule('all')"
>
Delete All
</button>
<button
type="button"
class="btn btn-sm"
:class="[temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']"
:disabled="temporaryData.length === 0"
@click="submitAll"
>
Apply All
</button>
</div>
</div>
</div>
</template>
<script setup>
@@ -49,30 +83,37 @@
* apply-all actions.
*/
import { storeToRefs } from 'pinia';
import { useToast } from 'vue-toast-notification';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import { delaySecond, } from '@/utils/timeUtil.js';
import { storeToRefs } from "pinia";
import { useToast } from "vue-toast-notification";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import { delaySecond } from "@/utils/timeUtil.js";
const emit = defineEmits(['submit-all']);
const emit = defineEmits(["submit-all"]);
const $toast = useToast();
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, tempFilterId } = storeToRefs(allMapDataStore);
const {
hasResultRule,
temporaryData,
postRuleData,
ruleData,
isRuleData,
tempFilterId,
} = storeToRefs(allMapDataStore);
/**
* Toggles a filter rule on or off.
* @param {boolean} e - Whether the rule is enabled.
* @param {number} index - The rule index.
*/
function isRule(e, index){
function isRule(e, index) {
const rule = isRuleData.value[index];
// First get the rule object
// To preserve data order, set the value to 0 and remove it during submitAll
if(!e) temporaryData.value[index] = 0;
if (!e) temporaryData.value[index] = 0;
else temporaryData.value[index] = rule;
}
@@ -81,20 +122,20 @@ function isRule(e, index){
* @param {number|string} index - The rule index, or 'all' to delete all.
*/
async function deleteRule(index) {
if(index === 'all') {
if (index === "all") {
temporaryData.value = [];
isRuleData.value = [];
ruleData.value = [];
if(tempFilterId.value) {
if (tempFilterId.value) {
isLoading.value = true;
tempFilterId.value = await null;
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
await emit('submit-all');
await emit("submit-all");
isLoading.value = false;
}
$toast.success('Filter(s) deleted.');
}else{
$toast.success("Filter(s) deleted.");
} else {
$toast.success(`Filter deleted.`);
temporaryData.value.splice(index, 1);
isRuleData.value.splice(index, 1);
@@ -104,23 +145,23 @@ async function deleteRule(index) {
/** Submits all enabled filter rules and refreshes the map data. */
async function submitAll() {
postRuleData.value = temporaryData.value.filter(item => item !== 0); // Get submit data; if toggle buttons are used, find and remove items set to 0
if(!postRuleData.value?.length) return $toast.error('Not selected');
postRuleData.value = temporaryData.value.filter((item) => item !== 0); // Get submit data; if toggle buttons are used, find and remove items set to 0
if (!postRuleData.value?.length) return $toast.error("Not selected");
await allMapDataStore.checkHasResult(); // Quick backend check for results
if(hasResultRule.value === null) {
if (hasResultRule.value === null) {
return;
} else if(hasResultRule.value) {
} else if (hasResultRule.value) {
isLoading.value = true;
await allMapDataStore.addTempFilterId();
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
if(temporaryData.value[0]?.type) {
if (temporaryData.value[0]?.type) {
allMapDataStore.traceId = await allMapDataStore.traces[0]?.id;
}
await emit('submit-all');
await emit("submit-all");
isLoading.value = false;
$toast.success('Filter(s) applied.');
$toast.success("Filter(s) applied.");
return;
}
@@ -128,7 +169,7 @@ async function submitAll() {
isLoading.value = true;
await delaySecond(1);
isLoading.value = false;
$toast.warning('No result.');
$toast.warning("No result.");
}
</script>
@@ -136,21 +177,21 @@ async function submitAll() {
@reference "../../../../assets/tailwind.css";
/* TimeLine */
:deep(.p-timeline) {
@apply leading-none my-4
@apply leading-none my-4;
}
:deep(.p-timeline-event-opposite) {
@apply hidden
@apply hidden;
}
:deep(.p-timeline-event-separator) {
@apply mx-4
@apply mx-4;
}
:deep(.p-timeline-event-marker) {
@apply !bg-primary !border-primary !w-2 !h-2
@apply !bg-primary !border-primary !w-2 !h-2;
}
:deep(.p-timeline-event-connector) {
@apply !bg-primary my-2 !w-[1px]
@apply !bg-primary my-2 !w-[1px];
}
:deep(.p-timeline-event-content) {
@apply !px-0
@apply !px-0;
}
</style>

View File

@@ -1,5 +1,7 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<section class="pt-2 pb-20 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
<div class="text-primary h2 flex items-center justify-start">
@@ -12,18 +14,48 @@
<div id="chart-mask-right" class="absolute bg-neutral-10/50"></div>
</div>
<div class="px-2 py-3">
<Slider v-model="selectArea" :step="1" :min="0" :max="selectRange" range class="mx-2" @change="changeSelectArea($event)"/>
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="selectRange"
range
class="mx-2"
@change="changeSelectArea($event)"
/>
</div>
<!-- Calendar group -->
<div class="flex justify-center items-center space-x-2 w-full">
<div>
<span class="block mb-2">Start time</span>
<Calendar v-model="startTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="startMinDate" :maxDate="startMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'start')" id="startCalendar"/>
<Calendar
v-model="startTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="startMinDate"
:maxDate="startMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderTimeRange($event, 'start')"
id="startCalendar"
/>
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar v-model="endTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="endMinDate" :maxDate="endMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'end')" id="endCalendar"/>
<Calendar
v-model="endTime"
dateFormat="yy/mm/dd"
:panelProps="panelProps"
:minDate="endMinDate"
:maxDate="endMaxDate"
showTime
showIcon
hourFormat="24"
@date-select="sliderTimeRange($event, 'end')"
id="endCalendar"
/>
</div>
</div>
<!-- End calendar group -->
@@ -41,14 +73,14 @@
* duration range selectors.
*/
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-moment';
import getMoment from 'moment';
import { ref, computed, watch, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { Chart, registerables } from "chart.js";
import "chartjs-adapter-moment";
import getMoment from "moment";
const props = defineProps(['selectValue']);
const props = defineProps(["selectValue"]);
const allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
@@ -71,8 +103,8 @@ const panelProps = ref({
// user select time start and end
const timeFrameStartEnd = computed(() => {
const start = getMoment(startTime.value).format('YYYY-MM-DDTHH:mm:00');
const end = getMoment(endTime.value).format('YYYY-MM-DDTHH:mm:00');
const start = getMoment(startTime.value).format("YYYY-MM-DDTHH:mm:00");
const end = getMoment(endTime.value).format("YYYY-MM-DDTHH:mm:00");
selectTimeFrame.value = [start, end]; // Data to send to the backend
return [start, end];
@@ -84,10 +116,10 @@ const sliderData = computed(() => {
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
const data = []
const data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + (step * i));
data.push(xAxisMin + step * i);
}
return data;
@@ -95,7 +127,7 @@ const sliderData = computed(() => {
// Add the minimum and maximum values
const timeFrameData = computed(() => {
const data = filterTimeframe.value.data.map(i=>({x:i.x,y:i.y}))
const data = filterTimeframe.value.data.map((i) => ({ x: i.x, y: i.y }));
// See ./public/timeFrameSlope for the y-axis slope calculation diagram
// x values are 0 ~ 11,
// Name three coordinates (ax, ay), (bx, by), (cx, cy) as (a, b), (c, d), (e, f)
@@ -109,8 +141,8 @@ const timeFrameData = computed(() => {
const d = filterTimeframe.value.data[0].y;
const e = 2;
const f = filterTimeframe.value.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a);
if(b < 0) {
b = (e * d - a * d - f * a - f * c) / (e - c - a);
if (b < 0) {
b = 0;
}
// Y-axis maximum value
@@ -119,8 +151,8 @@ const timeFrameData = computed(() => {
const mc = 10;
const md = filterTimeframe.value.data[9].y;
const me = 11;
let mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
if(mf < 0) {
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
if (mf < 0) {
mf = 0;
}
@@ -128,12 +160,12 @@ const timeFrameData = computed(() => {
data.unshift({
x: filterTimeframe.value.x_axis.min_base,
y: b,
})
});
// Add the maximum value
data.push({
x: filterTimeframe.value.x_axis.max_base,
y: mf,
})
});
return data;
});
@@ -144,7 +176,7 @@ const labelsData = computed(() => {
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for(let i = 0; i< numPoints; i++) {
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
@@ -152,7 +184,7 @@ const labelsData = computed(() => {
});
watch(selectTimeFrame, (newValue, oldValue) => {
if(newValue.length === 0) {
if (newValue.length === 0) {
startTime.value = new Date(filterTimeframe.value.x_axis.min);
endTime.value = new Date(filterTimeframe.value.x_axis.max);
selectArea.value = [0, selectRange.value];
@@ -167,7 +199,7 @@ watch(selectTimeFrame, (newValue, oldValue) => {
function resizeMask(chartInstance) {
const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
if(props.selectValue[0] === 'Timeframes') {
if (props.selectValue[0] === "Timeframes") {
resizeLeftMask(chartInstance, from);
resizeRightMask(chartInstance, to);
}
@@ -208,20 +240,20 @@ function createChart() {
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
const data = {
labels:labelsData.value,
labels: labelsData.value,
datasets: [
{
label: 'Case',
label: "Case",
data: timeFrameData.value,
fill: 'start',
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
x: 'x',
y: 'y',
}
]
x: "x",
y: "y",
},
],
};
const options = {
responsive: true,
@@ -231,66 +263,67 @@ function createChart() {
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false
propagate: false,
},
title: false
title: false,
},
// animations: false, // Disable animations
animation: {
onComplete: e => {
onComplete: (e) => {
resizeMask(e.chart);
}
},
},
interaction: {
intersect: true,
},
scales: {
x: {
type: 'time',
type: "time",
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // Do not rotate labels (0~50)
color: '#334155',
color: "#334155",
display: true,
source: 'labels',
source: "labels",
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
minUnit: 'day', // Minimum display unit
minUnit: "day", // Minimum display unit
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
}
},
},
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
},
},
},
};
const config = {
type: 'line',
const config = {
type: "line",
data: data,
options: options,
};
@@ -327,22 +360,29 @@ function changeSelectArea(e) {
function sliderTimeRange(e, direction) {
// Find the closest index; time format: millisecond timestamps
const sliderDataVal = sliderData.value;
const targetTime = [new Date(timeFrameStartEnd.value[0]).getTime(), new Date(timeFrameStartEnd.value[1]).getTime()];
const closestIndexes = targetTime.map(target => {
const targetTime = [
new Date(timeFrameStartEnd.value[0]).getTime(),
new Date(timeFrameStartEnd.value[1]).getTime(),
];
const closestIndexes = targetTime.map((target) => {
let closestIndex = 0;
closestIndex = ((target - sliderDataVal[0])/(sliderDataVal[sliderDataVal.length-1]-sliderDataVal[0])) * sliderDataVal.length;
closestIndex =
((target - sliderDataVal[0]) /
(sliderDataVal[sliderDataVal.length - 1] - sliderDataVal[0])) *
sliderDataVal.length;
let result = Math.round(Math.abs(closestIndex));
result = result > selectRange.value ? selectRange.value : result;
return result
return result;
});
// Update the slider
selectArea.value = closestIndexes;
// Reset the start/end calendar selection range
if(direction === 'start') endMinDate.value = e;
else if(direction === 'end') startMaxDate.value = e;
if (direction === "start") endMinDate.value = e;
else if (direction === "end") startMaxDate.value = e;
// Recalculate the chart mask
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) resizeMask(chart.value);
if (!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1]))
resizeMask(chart.value);
else return;
}

View File

@@ -1,5 +1,7 @@
<template>
<div class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar">
<div
class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar"
>
<!-- Range Selection -->
<section class="py-2 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
@@ -7,32 +9,64 @@
<span class="material-symbols-outlined mr-2 !text-base">info</span>
<p>Select a percentage range.</p>
</div>
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-2/5" />
<Chart
type="bar"
:data="chartData"
:options="chartOptions"
class="h-2/5"
/>
<div class="px-2">
<p class="py-4">Select percentage of case <span class=" float-right">{{ caseTotalPercent }}%</span></p>
<Slider v-model="selectArea" :step="1" :min="0" :max="traceTotal" range class="mx-2" />
<p class="py-4">
Select percentage of case
<span class="float-right">{{ caseTotalPercent }}%</span>
</p>
<Slider
v-model="selectArea"
:step="1"
:min="0"
:max="traceTotal"
range
class="mx-2"
/>
</div>
</section>
<!-- Trace List -->
<section class="h-full min-w-[48%] py-2 space-y-2">
<p class="h2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 flex items-center justify-start">
<span class="material-symbols-outlined mr-2 !text-base">info</span>Click trace number to see more.
<span class="material-symbols-outlined mr-2 !text-base">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)]" >
<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 w-full">
<caption class="hidden">Trace list</caption>
<caption class="hidden">
Trace list
</caption>
<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>
<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)">
<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 text-center">#{{ trace.id }}</td>
<td class="p-2 min-w-[96px]">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<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>
@@ -48,16 +82,34 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
<div
id="cyTrace"
ref="cyTraceRef"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] infiniteTable" @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
<div
class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_200px)] 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">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
{{ data[col.field] }}
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
@@ -65,7 +117,7 @@
</DataTable>
</div>
</section>
</div>
</div>
</template>
<script setup>
@@ -79,22 +131,28 @@
* trace detail display.
*/
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useLoadingStore } from '@/stores/loading';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useLoadingStore } from "@/stores/loading";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const emit = defineEmits(['filter-trace-selectArea']);
const emit = defineEmits(["filter-trace-selectArea"]);
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const { infinit404, baseInfiniteStart, baseTraces, baseTraceTaskSeq, baseCases } = storeToRefs(allMapDataStore);
const {
infinit404,
baseInfiniteStart,
baseTraces,
baseTraceTaskSeq,
baseCases,
} = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
@@ -111,95 +169,113 @@ const traceTotal = computed(() => {
defineExpose({ selectArea, showTraceId, traceTotal });
const traceCountTotal = computed(() => {
return baseTraces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
return baseTraces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
});
const traceList = computed(() => {
return baseTraces.value.map(trace => {
return {
id: trace.id,
value: progressWidth(Number(((trace.count / traceCountTotal.value) * 100).toFixed(1))),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
}).slice(selectArea.value[0], selectArea.value[1]);
return baseTraces.value
.map((trace) => {
return {
id: trace.id,
value: progressWidth(
Number(((trace.count / traceCountTotal.value) * 100).toFixed(1)),
),
count: trace.count.toLocaleString(),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
})
.slice(selectArea.value[0], selectArea.value[1]);
});
const caseTotalPercent = computed(() => {
const ratioSum = traceList.value.map(trace => trace.base_count).reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
return getPercentLabel(ratioSum)
const ratioSum =
traceList.value
.map((trace) => trace.base_count)
.reduce((acc, cur) => acc + cur, 0) / traceCountTotal.value;
return getPercentLabel(ratioSum);
});
const chartData = computed(() => {
const start = selectArea.value[0];
const end = selectArea.value[1] - 1;
const labels = baseTraces.value.map(trace => `#${trace.id}`);
const data = baseTraces.value.map(trace => getPercentLabel(trace.count / traceCountTotal.value));
const selectAreaData = baseTraces.value.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
const labels = baseTraces.value.map((trace) => `#${trace.id}`);
const data = baseTraces.value.map((trace) =>
getPercentLabel(trace.count / traceCountTotal.value),
);
const selectAreaData = baseTraces.value.map((trace, index) =>
index >= start && index <= end ? "rgba(0,153,255)" : "rgba(203, 213, 225)",
);
return { // Data to display
return {
// Data to display
labels,
datasets: [
{
label: 'Trace', // Dataset label
label: "Trace", // Dataset label
data,
backgroundColor: selectAreaData,
categoryPercentage: 1.0,
barPercentage: 1.0
barPercentage: 1.0,
},
]
],
};
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(baseCases.value)); // Deep copy the original cases data
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
{ 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 })),
{ 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
return result;
});
watch(selectArea, (newValue, oldValue) => {
const roundValue = Math.round(newValue[1].toFixed());
if(newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if(newValue != oldValue) emit('filter-trace-selectArea', newValue); // Determine whether Apply should be disabled
if (newValue[1] !== roundValue) selectArea.value[1] = roundValue;
if (newValue != oldValue) emit("filter-trace-selectArea", newValue); // Determine whether Apply should be disabled
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
if (newValue === 404) infinitMaxItems.value = true;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
/**
* Set bar chart Options
*/
function barOptions(){
function barOptions() {
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
@@ -208,41 +284,43 @@ function barOptions(){
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: { // Legend
legend: {
// Legend
display: false,
},
tooltip: {
callbacks: {
label: (tooltipItems) =>{
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`
}
}
}
label: (tooltipItems) => {
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`;
},
},
},
},
animations: false,
scales: {
x: {
display:false
display: false,
},
y: {
ticks: { // Set tick intervals
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
min: 0,
max: traceList.value[0]?.ratio,
stepSize: (traceList.value[0]?.ratio)/4,
stepSize: traceList.value[0]?.ratio / 4,
},
grid: {
color: 'rgba(100,116,139)',
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
}
}
}
},
},
},
};
}
@@ -251,8 +329,8 @@ function barOptions(){
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
@@ -261,8 +339,8 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -272,7 +350,7 @@ function progressWidth(value){
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
isLoading.value = true; // Always show loading screen
infinit404.value = null;
infinitMaxItems.value = false;
@@ -287,7 +365,7 @@ async function switchCaseData(id, count) {
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
@@ -296,20 +374,20 @@ function setNodesData(){
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
})
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
baseTraceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
@@ -317,8 +395,8 @@ function setEdgesData(){
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
// The number of edges is one less than the number of nodes
@@ -328,7 +406,7 @@ function setEdgesData(){
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
const graphId = cyTraceRef.value;
setNodesData();
@@ -341,12 +419,18 @@ function createCy(){
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(infinitMaxItems.value || baseCases.value.length < 20 || infiniteFinish.value === false) return;
if (
infinitMaxItems.value ||
baseCases.value.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
if (overScrollHeight) fetchData();
}
/**
@@ -361,8 +445,8 @@ async function fetchData() {
infiniteData.value = [...infiniteData.value, ...baseCases.value];
infiniteFinish.value = true;
isLoading.value = false;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
@@ -372,7 +456,7 @@ onMounted(() => {
setEdgesData();
createCy();
chartOptions.value = barOptions();
selectArea.value = [0, traceTotal.value]
selectArea.value = [0, traceTotal.value];
isLoading.value = false;
});
</script>
@@ -381,14 +465,14 @@ onMounted(() => {
@reference "../../../../assets/tailwind.css";
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@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;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
@@ -398,6 +482,6 @@ onMounted(() => {
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,13 @@
<template>
<Sidebar :visible="sidebarTraces" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="false" class="!w-11/12" @show="show()">
<Sidebar
:visible="sidebarTraces"
:closeIcon="'pi pi-chevron-left'"
:modal="false"
position="left"
:dismissable="false"
class="!w-11/12"
@show="show()"
>
<template #header>
<p class="h1">Traces</p>
</template>
@@ -7,23 +15,37 @@
<!-- 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">
Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<p class="text-primary h2 px-2 mb-2">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">
<caption class="hidden">Trace List</caption>
<caption class="hidden">
Trace List
</caption>
<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>
<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)">
<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-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
@@ -39,16 +61,34 @@
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-36 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTraceRef" class="h-full min-w-full relative"></div>
<div
id="cyTrace"
ref="cyTraceRef"
class="h-full min-w-full relative"
></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] infiniteTable " @scroll="handleScroll">
<DataTable :value="caseData" showGridlines tableClass="text-sm" breakpoint="0">
<div
class="overflow-y-auto overflow-x-auto scrollbar w-full h-[calc(100%_-_200px)] 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">
<template #body="{ data }">
<div :class="data[col.field]?.length > 18 ? 'whitespace-normal' : 'whitespace-nowrap'">
{{ data[col.field] }}
<div
:class="
data[col.field]?.length > 18
? 'whitespace-normal'
: 'whitespace-nowrap'
"
>
{{ data[col.field] }}
</div>
</template>
</Column>
@@ -70,23 +110,30 @@
* clickable trace lists for highlighting on the map.
*/
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { ref, computed, watch } from "vue";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
const props = defineProps(['sidebarTraces', 'cases']);
const emit = defineEmits(['switch-Trace-Id']);
const props = defineProps(["sidebarTraces", "cases"]);
const emit = defineEmits(["switch-Trace-Id"]);
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { infinit404, infiniteStart, traceId, traces, traceTaskSeq, infiniteFirstCases } = storeToRefs(allMapDataStore);
const {
infinit404,
infiniteStart,
traceId,
traces,
traceTaskSeq,
infiniteFirstCases,
} = storeToRefs(allMapDataStore);
const processMap = ref({
nodes:[],
edges:[],
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
@@ -99,8 +146,10 @@ const traceTotal = computed(() => {
});
const traceList = computed(() => {
const sum = traces.value.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
const result = traces.value.map(trace => {
const sum = traces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
const result = traces.value.map((trace) => {
return {
id: trace.id,
value: progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
@@ -108,54 +157,63 @@ const traceList = computed(() => {
base_count: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
})
});
return result;
});
const caseData = computed(() => {
const data = JSON.parse(JSON.stringify(infiniteData.value)); // Deep copy the original cases data
data.forEach(item => {
data.forEach((item) => {
item.attributes.forEach((attribute, index) => {
item[`att_${index}`] = attribute.value; // Create a new key-value pair
});
delete item.attributes; // Remove the original attributes property
})
});
return data;
});
const columnData = computed(() => {
const data = JSON.parse(JSON.stringify(props.cases)); // Deep copy the original cases data
let result = [
{ field: 'id', header: 'Case Id' },
{ field: 'started_at', header: 'Start time' },
{ field: 'completed_at', header: 'End time' },
];
if(data.length !== 0){
{ 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 })),
{ 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
return result;
});
watch(infinit404, (newValue) => {
if(newValue === 404) infinitMaxItems.value = true;
if (newValue === 404) infinitMaxItems.value = true;
});
watch(traceId, (newValue) => {
showTraceId.value = newValue;
}, { immediate: true });
watch(
traceId,
(newValue) => {
showTraceId.value = newValue;
},
{ immediate: true },
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector('.infiniteTable');
if(isScrollTop && typeof isScrollTop.scrollTop !== 'undefined') if(newValue !== oldValue) isScrollTop.scrollTop = 0;
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && typeof isScrollTop.scrollTop !== "undefined")
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(infiniteFirstCases, (newValue) => {
if(infiniteFirstCases.value) infiniteData.value = JSON.parse(JSON.stringify(newValue));
if (infiniteFirstCases.value)
infiniteData.value = JSON.parse(JSON.stringify(newValue));
});
/**
@@ -163,8 +221,8 @@ watch(infiniteFirstCases, (newValue) => {
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return `100%`;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
@@ -173,8 +231,8 @@ function getPercentLabel(val){
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value){
return `width:${value}%;`
function progressWidth(value) {
return `width:${value}%;`;
}
/**
@@ -184,19 +242,19 @@ function progressWidth(value){
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
if(id == showTraceId.value) return;
if (id == showTraceId.value) return;
isLoading.value = true; // Always show loading screen
infinit404.value = null;
infinitMaxItems.value = false;
showTraceId.value = id;
infiniteStart.value = 0;
emit('switch-Trace-Id', {id: showTraceId.value, count: count}); // Pass to Map index, which will close loading
emit("switch-Trace-Id", { id: showTraceId.value, count: count }); // Pass to Map index, which will close loading
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData(){
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
@@ -205,20 +263,20 @@ function setNodesData(){
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100
}
width: 100,
},
});
})
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData(){
function setEdgesData() {
processMap.value.edges = [];
traceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
@@ -226,8 +284,8 @@ function setEdgesData(){
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
style: "solid",
},
});
});
// The number of edges is one less than the number of nodes
@@ -237,7 +295,7 @@ function setEdgesData(){
/**
* create trace cytoscape's map
*/
function createCy(){
function createCy() {
const graphId = cyTraceRef.value;
setNodesData();
@@ -264,12 +322,18 @@ async function show() {
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if(infinitMaxItems.value || props.cases.length < 20 || infiniteFinish.value === false) return;
if (
infinitMaxItems.value ||
props.cases.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if(overScrollHeight) fetchData();
if (overScrollHeight) fetchData();
}
/**
@@ -284,8 +348,8 @@ async function fetchData() {
infiniteData.value = [...infiniteData.value, ...props.cases];
infiniteFinish.value = true;
isLoading.value = false;
} catch(error) {
console.error('Failed to load data:', error);
} catch (error) {
console.error("Failed to load data:", error);
}
}
</script>
@@ -294,18 +358,18 @@ async function fetchData() {
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
@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;
white-space: nowrap;
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0 text-center
@apply border-neutral-500 !border-t-0 text-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
@@ -315,6 +379,6 @@ async function fetchData() {
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center
@apply justify-center;
}
</style>

View File

@@ -1,5 +1,11 @@
<template>
<Sidebar :visible="sidebarView" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="false" >
<Sidebar
:visible="sidebarView"
:closeIcon="'pi pi-chevron-left'"
:modal="false"
position="left"
:dismissable="false"
>
<template #header>
<p class="h1">Visualization Setting</p>
</template>
@@ -10,28 +16,54 @@
<ul class="space-y-3 mb-4">
<!-- Select bpmn / processmap button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="onProcessMapClick()">
<span
class="btn-toggle-item"
:class="mapType === 'processMap' ? 'btn-toggle-show ' : ''"
@click="onProcessMapClick()"
>
Process Map
</span>
<span class="btn-toggle-item" :class="mapType === 'bpmn'?'btn-toggle-show':''" @click="onBPMNClick()">
<span
class="btn-toggle-item"
:class="mapType === 'bpmn' ? 'btn-toggle-show' : ''"
@click="onBPMNClick()"
>
BPMN Model
</span>
</li>
<!-- Select drawing style: bezier / unbundled-bezier button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="curveStyle === 'unbundled-bezier'?'btn-toggle-show ':''" @click="switchCurveStyles('unbundled-bezier')">
<span
class="btn-toggle-item"
:class="
curveStyle === 'unbundled-bezier' ? 'btn-toggle-show ' : ''
"
@click="switchCurveStyles('unbundled-bezier')"
>
Curved
</span>
<span class="btn-toggle-item" :class="curveStyle === 'taxi'?'btn-toggle-show':''" @click="switchCurveStyles('taxi')">
<span
class="btn-toggle-item"
:class="curveStyle === 'taxi' ? 'btn-toggle-show' : ''"
@click="switchCurveStyles('taxi')"
>
Elbow
</span>
</li>
<!-- Vertical TB | Horizontal LR -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="rank === 'LR'?'btn-toggle-show ':''" @click="switchRank('LR')">
<span
class="btn-toggle-item"
:class="rank === 'LR' ? 'btn-toggle-show ' : ''"
@click="switchRank('LR')"
>
Horizontal
</span>
<span class="btn-toggle-item" :class="rank === 'TB'?'btn-toggle-show':''" @click="switchRank('TB')">
<span
class="btn-toggle-item"
:class="rank === 'TB' ? 'btn-toggle-show' : ''"
@click="switchRank('TB')"
>
Vertical
</span>
</li>
@@ -41,25 +73,67 @@
<div>
<p class="h2">Data Layer</p>
<ul class="space-y-2">
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'freq')">
<li
class="flex justify-between mb-3"
@change="switchDataLayerType($event, 'freq')"
>
<div class="flex items-center w-1/2">
<RadioButton v-model="dataLayerType" inputId="freq" name="dataLayer" value="freq" class="mr-2" @click.prevent="switchDataLayerType($event, 'freq')"/>
<RadioButton
v-model="dataLayerType"
inputId="freq"
name="dataLayer"
value="freq"
class="mr-2"
@click.prevent="switchDataLayerType($event, 'freq')"
/>
<label for="freq">Frequency</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'duration'">
<option v-for="(freq, index) in selectFrequency" :key="index" :value="freq.value" :disabled="freq.disabled" :selected="freq.value === selectedFreq">{{ freq.label }}</option>
<select
class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary"
:disabled="dataLayerType === 'duration'"
>
<option
v-for="(freq, index) in selectFrequency"
:key="index"
:value="freq.value"
:disabled="freq.disabled"
:selected="freq.value === selectedFreq"
>
{{ freq.label }}
</option>
</select>
</div>
</li>
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'duration')">
<li
class="flex justify-between mb-3"
@change="switchDataLayerType($event, 'duration')"
>
<div class="flex items-center w-1/2">
<RadioButton v-model="dataLayerType" inputId="duration" name="dataLayer" value="duration" class="mr-2" @click.prevent="switchDataLayerType($event, 'duration')"/>
<RadioButton
v-model="dataLayerType"
inputId="duration"
name="dataLayer"
value="duration"
class="mr-2"
@click.prevent="switchDataLayerType($event, 'duration')"
/>
<label for="duration">Duration</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'freq'">
<option v-for="(duration, index) in selectDuration" :key="index" :value="duration.value" :disabled="duration.disabled" :selected="duration.value === selectedDuration">{{ duration.label }}</option>
<select
class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary"
:disabled="dataLayerType === 'freq'"
>
<option
v-for="(duration, index) in selectDuration"
:key="index"
:value="duration.value"
:disabled="duration.disabled"
:selected="duration.value === selectedDuration"
>
{{ duration.label }}
</option>
</select>
</div>
</li>
@@ -80,9 +154,9 @@
* style, direction, and data layer selection.
*/
import { ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useMapPathStore } from '@/stores/mapPathStore';
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useMapPathStore } from "@/stores/mapPathStore";
defineProps({
sidebarView: {
@@ -92,39 +166,39 @@ defineProps({
});
const emit = defineEmits([
'switch-map-type',
'switch-curve-styles',
'switch-rank',
'switch-data-layer-type',
"switch-map-type",
"switch-curve-styles",
"switch-rank",
"switch-data-layer-type",
]);
const mapPathStore = useMapPathStore();
const { isBPMNOn } = storeToRefs(mapPathStore);
const selectFrequency = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_freq", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value:"cases", label:"Number of cases", disabled:false, },
{ value: "total", label: "Total", disabled: false },
{ value: "rel_freq", label: "Relative", disabled: false },
{ value: "average", label: "Average", disabled: false },
{ value: "median", label: "Median", disabled: false },
{ value: "max", label: "Max", disabled: false },
{ value: "min", label: "Min", disabled: false },
{ value: "cases", label: "Number of cases", disabled: false },
]);
const selectDuration = ref([
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_duration", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value: "total", label: "Total", disabled: false },
{ value: "rel_duration", label: "Relative", disabled: false },
{ value: "average", label: "Average", disabled: false },
{ value: "median", label: "Median", disabled: false },
{ value: "max", label: "Max", disabled: false },
{ value: "min", label: "Min", disabled: false },
]);
const curveStyle = ref('unbundled-bezier'); // unbundled-bezier | taxi
const mapType = ref('processMap'); // processMap | bpmn
const curveStyle = ref("unbundled-bezier"); // unbundled-bezier | taxi
const mapType = ref("processMap"); // processMap | bpmn
const dataLayerType = ref(null); // freq | duration
const dataLayerOption = ref(null);
const selectedFreq = ref('');
const selectedDuration = ref('');
const rank = ref('LR'); // Vertical TB | Horizontal LR
const selectedFreq = ref("");
const selectedDuration = ref("");
const rank = ref("LR"); // Vertical TB | Horizontal LR
/**
* Switches the map type and emits the change event.
@@ -132,7 +206,7 @@ const rank = ref('LR'); // Vertical TB | Horizontal LR
*/
function switchMapType(type) {
mapType.value = type;
emit('switch-map-type', mapType.value);
emit("switch-map-type", mapType.value);
}
/**
@@ -141,7 +215,7 @@ function switchMapType(type) {
*/
function switchCurveStyles(style) {
curveStyle.value = style;
emit('switch-curve-styles', curveStyle.value);
emit("switch-curve-styles", curveStyle.value);
}
/**
@@ -150,7 +224,7 @@ function switchCurveStyles(style) {
*/
function switchRank(rankValue) {
rank.value = rankValue;
emit('switch-rank', rank.value);
emit("switch-rank", rank.value);
}
/**
@@ -159,40 +233,41 @@ function switchRank(rankValue) {
* @param {string} type - 'freq' or 'duration'.
*/
function switchDataLayerType(e, type) {
let value = '';
let value = "";
if(e.target.value !== 'freq' && e.target.value !== 'duration') value = e.target.value;
if (e.target.value !== "freq" && e.target.value !== "duration")
value = e.target.value;
switch (type) {
case 'freq':
value = value || selectedFreq.value || 'total';
case "freq":
value = value || selectedFreq.value || "total";
dataLayerType.value = type;
dataLayerOption.value = value;
selectedFreq.value = value;
break;
case 'duration':
value = value || selectedDuration.value || 'total';
case "duration":
value = value || selectedDuration.value || "total";
dataLayerType.value = type;
dataLayerOption.value = value;
selectedDuration.value = value;
break;
}
emit('switch-data-layer-type', dataLayerType.value, dataLayerOption.value);
emit("switch-data-layer-type", dataLayerType.value, dataLayerOption.value);
}
/** Switches to Process Map view. */
function onProcessMapClick() {
mapPathStore.setIsBPMNOn(false);
switchMapType('processMap');
switchMapType("processMap");
}
/** Switches to BPMN Model view. */
function onBPMNClick() {
mapPathStore.setIsBPMNOn(true);
switchMapType('bpmn');
switchMapType("bpmn");
}
onMounted(() => {
dataLayerType.value = 'freq';
dataLayerOption.value = 'total';
dataLayerType.value = "freq";
dataLayerOption.value = "total";
});
</script>

View File

@@ -1,81 +1,149 @@
<template>
<section class="w-full top-0 absolute shadow-[0px_6px_6px_inset_rgba(0,0,0,0.1)] z-20">
<!-- status content -->
<ul class="bg-neutral-100 flex justify-start shadow-[0px_1px_4px_rgba(0,0,0,0.2)] gap-3 p-3 text-sm overflow-x-auto scrollbar duration-700" v-show="isPanel" v-if="statData">
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Cases</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.cases.count }} / {{ statData.cases.total }}</span>
<ProgressBar :value="statData.cases.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<section
class="w-full top-0 absolute shadow-[0px_6px_6px_inset_rgba(0,0,0,0.1)] z-20"
>
<!-- status content -->
<ul
class="bg-neutral-100 flex justify-start shadow-[0px_1px_4px_rgba(0,0,0,0.2)] gap-3 p-3 text-sm overflow-x-auto scrollbar duration-700"
v-show="isPanel"
v-if="statData"
>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Cases</p>
</div>
<span class="block text-2xl font-medium">{{ statData.cases.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Traces</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.traces.count }} / {{ statData.traces.total }}</span>
<ProgressBar :value="statData.traces.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.cases.count }} / {{ statData.cases.total }}</span
>
<ProgressBar
:value="statData.cases.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.cases.ratio }}%</span
>
</div>
<span class="block text-2xl font-medium">{{ statData.traces.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activity Instances</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.task_instances.count }} / {{ statData.task_instances.total }}</span>
<ProgressBar :value="statData.task_instances.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Traces</p>
</div>
<span class="block text-2xl font-medium">{{ statData.task_instances.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activities</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2">{{ statData.tasks.count }} / {{ statData.tasks.total }}</span>
<ProgressBar :value="statData.tasks.ratio" :showValue="false" class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"></ProgressBar>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.traces.count }} / {{ statData.traces.total }}</span
>
<ProgressBar
:value="statData.traces.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.traces.ratio }}%</span
>
</div>
<span class="block text-2xl font-medium">{{ statData.tasks.ratio }}%</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8 mb-2.5">Log Timeframe</p>
<div class="px-2 space-y-2 min-w-[140px] h-[40px]">
<span class="inline-block">{{ statData.started_at }}&nbsp</span>
<span class="inline-block">~&nbsp{{ statData.completed_at }}</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8">Case Duration</p>
<div class="flex justify-between items-center space-x-2 min-w-[272px]">
<div class="space-y-2">
<p><Tag value="MAX" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.max }}</p>
<p><Tag value="MIN" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.min }}</p>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activity Instances</p>
</div>
<div class="space-y-2">
<p><Tag value="MED" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.median }}</p>
<p><Tag value="AVG" class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"></Tag>{{ statData.case_duration.average }}</p>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.task_instances.count }} /
{{ statData.task_instances.total }}</span
>
<ProgressBar
:value="statData.task_instances.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.task_instances.ratio }}%</span
>
</div>
</div>
</li>
</ul>
<!-- control button -->
<div class="bg-neutral-300 rounded-b-full w-20 text-center mx-auto cursor-pointer hover:bg-neutral-500 hover:text-neutral-10 active:ring focus:outline-none focus:border-neutral-500 focus:ring" @click="isPanel = !isPanel">
<span class="material-symbols-outlined block px-8 !text-xs ">{{ isPanel ? 'keyboard_double_arrow_up' : 'keyboard_double_arrow_down' }}</span>
</div>
</section>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<div class="flex justify-between items-center mb-5">
<p class="font-bold text-sm leading-8">Activities</p>
</div>
<div class="flex justify-between items-center">
<div class="mr-2 w-full">
<span class="block text-sm mb-2"
>{{ statData.tasks.count }} / {{ statData.tasks.total }}</span
>
<ProgressBar
:value="statData.tasks.ratio"
:showValue="false"
class="!h-1.5 min-w-[136px] rounded !bg-neutral-200"
></ProgressBar>
</div>
<span class="block text-2xl font-medium"
>{{ statData.tasks.ratio }}%</span
>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8 mb-2.5">Log Timeframe</p>
<div class="px-2 space-y-2 min-w-[140px] h-[40px]">
<span class="inline-block">{{ statData.started_at }}&nbsp</span>
<span class="inline-block">~&nbsp{{ statData.completed_at }}</span>
</div>
</li>
<li class="bg-neutral-10 rounded p-3 w-full">
<p class="font-bold text-sm leading-8">Case Duration</p>
<div class="flex justify-between items-center space-x-2 min-w-[272px]">
<div class="space-y-2">
<p>
<Tag
value="MAX"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.max }}
</p>
<p>
<Tag
value="MIN"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.min }}
</p>
</div>
<div class="space-y-2">
<p>
<Tag
value="MED"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.median }}
</p>
<p>
<Tag
value="AVG"
class="!text-neutral-900 !bg-neutral-300 mr-2 !w-10 !text-sm !px-2 !py-0"
></Tag
>{{ statData.case_duration.average }}
</p>
</div>
</div>
</li>
</ul>
<!-- control button -->
<div
class="bg-neutral-300 rounded-b-full w-20 text-center mx-auto cursor-pointer hover:bg-neutral-500 hover:text-neutral-10 active:ring focus:outline-none focus:border-neutral-500 focus:ring"
@click="isPanel = !isPanel"
>
<span class="material-symbols-outlined block px-8 !text-xs">{{
isPanel ? "keyboard_double_arrow_up" : "keyboard_double_arrow_down"
}}</span>
</div>
</section>
</template>
<script setup>
@@ -89,12 +157,12 @@
* timeframe, case duration) for the Discover page.
*/
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useAllMapDataStore } from '@/stores/allMapData';
import { getTimeLabel } from '@/module/timeLabel.js';
import getMoment from 'moment';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { getTimeLabel } from "@/module/timeLabel.js";
import getMoment from "moment";
const route = useRoute();
@@ -109,8 +177,8 @@ const statData = ref(null);
* @param {number} val - The ratio value to convert.
* @returns {number} The percentage value.
*/
function getPercentLabel(val){
if((val * 100).toFixed(1) >= 100) return 100;
function getPercentLabel(val) {
if ((val * 100).toFixed(1) >= 100) return 100;
else return parseFloat((val * 100).toFixed(1));
}
@@ -118,46 +186,48 @@ function getPercentLabel(val){
function getStatData() {
statData.value = {
cases: {
count: stats.value.cases.count.toLocaleString('en-US'),
total: stats.value.cases.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.cases.ratio)
count: stats.value.cases.count.toLocaleString("en-US"),
total: stats.value.cases.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.cases.ratio),
},
traces: {
count: stats.value.traces.count.toLocaleString('en-US'),
total: stats.value.traces.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.traces.ratio)
count: stats.value.traces.count.toLocaleString("en-US"),
total: stats.value.traces.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.traces.ratio),
},
task_instances: {
count: stats.value.task_instances.count.toLocaleString('en-US'),
total: stats.value.task_instances.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.task_instances.ratio)
count: stats.value.task_instances.count.toLocaleString("en-US"),
total: stats.value.task_instances.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.task_instances.ratio),
},
tasks: {
count: stats.value.tasks.count.toLocaleString('en-US'),
total: stats.value.tasks.total.toLocaleString('en-US'),
ratio: getPercentLabel(stats.value.tasks.ratio)
count: stats.value.tasks.count.toLocaleString("en-US"),
total: stats.value.tasks.total.toLocaleString("en-US"),
ratio: getPercentLabel(stats.value.tasks.ratio),
},
started_at: getMoment(stats.value.started_at).format('YYYY-MM-DD HH:mm'),
completed_at: getMoment(stats.value.completed_at).format('YYYY-MM-DD HH:mm'),
started_at: getMoment(stats.value.started_at).format("YYYY-MM-DD HH:mm"),
completed_at: getMoment(stats.value.completed_at).format(
"YYYY-MM-DD HH:mm",
),
case_duration: {
min: getTimeLabel(stats.value.case_duration.min),
max: getTimeLabel(stats.value.case_duration.max),
average: getTimeLabel(stats.value.case_duration.average),
median: getTimeLabel(stats.value.case_duration.median),
}
}
},
};
}
onMounted(async () => {
const params = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
const isCheckPage = route.name.includes("Check");
switch (params.type) {
case 'log':
case "log":
logId.value = isCheckPage ? file.parent.id : params.fileId;
break;
case 'filter':
case "filter":
createFilterId.value = isCheckPage ? file.parent.id : params.fileId;
break;
}
@@ -169,6 +239,6 @@ onMounted(async () => {
<style scoped>
@reference "../../assets/tailwind.css";
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-neutral-900
@apply bg-neutral-900;
}
</style>

View File

@@ -1,15 +1,30 @@
<template>
<Dialog :visible="uploadModal" modal :style="{ width: '90vw', height: '90vh' }" :contentClass="contentClass" @update:visible="emit('closeModal', $event)">
<Dialog
:visible="uploadModal"
modal
:style="{ width: '90vw', height: '90vh' }"
:contentClass="contentClass"
@update:visible="emit('closeModal', $event)"
>
<template #header>
<div class="py-5">
</div>
<div class="py-5"></div>
</template>
<label for="uploadFiles">
<div class=" h-full flex flex-col justify-center items-center p-4 space-y-4 relative">
<input id="uploadFiles" class=" absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" type="file" accept="text/csv" @change="upload($event)">
<div
class="h-full flex flex-col justify-center items-center p-4 space-y-4 relative"
>
<input
id="uploadFiles"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
type="file"
accept="text/csv"
@change="upload($event)"
/>
<IconUploarding class="loader-arrow-upward"></IconUploarding>
<p class="text-neutral-900">Click or drag a file here.</p>
<p class="text-neutral-500">(Only <span class="text-primary">.csv</span> is supported)</p>
<p class="text-neutral-500">
(Only <span class="text-primary">.csv</span> is supported)
</p>
</div>
</label>
</Dialog>
@@ -26,59 +41,59 @@
* with drag-and-drop support for CSV files (max 90 MB).
*/
import { onBeforeUnmount, } from 'vue';
import { storeToRefs } from 'pinia';
import IconUploarding from '../icons/IconUploarding.vue';
import { uploadFailedFirst } from '@/module/alertModal.js';
import { useFilesStore } from '@/stores/files';
import { onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import IconUploarding from "../icons/IconUploarding.vue";
import { uploadFailedFirst } from "@/module/alertModal.js";
import { useFilesStore } from "@/stores/files";
defineProps(['uploadModal']);
const emit = defineEmits(['closeModal']);
defineProps(["uploadModal"]);
const emit = defineEmits(["closeModal"]);
const filesStore = useFilesStore();
const { uploadFileName } = storeToRefs(filesStore);
const contentClass = 'h-full';
const contentClass = "h-full";
/**
* Handles CSV file upload: validates size, sends to API, extracts filename.
* @param {Event} event - The file input change event.
*/
async function upload(event) {
const fileInput = document.getElementById('uploadFiles');
const fileInput = document.getElementById("uploadFiles");
const target = event.target;
const formData = new FormData();
let uploadFile;
// Check if a file exists
if(target && target.files) {
if (target && target.files) {
uploadFile = target.files[0];
}
// File size must not exceed 90 MB (90*1024*1024 = 94,371,840 bytes)
if(uploadFile.size >= 94371840) {
fileInput.value = '';
return uploadFailedFirst('size');
if (uploadFile.size >= 94371840) {
fileInput.value = "";
return uploadFailedFirst("size");
}
// Append the file to formData; the field name must be "csv"
formData.append('csv', uploadFile);
formData.append("csv", uploadFile);
// Call the first-stage upload API
if(uploadFile) {
if (uploadFile) {
await filesStore.upload(formData);
}
if (uploadFile.name.endsWith('.csv')) {
if (uploadFile.name.endsWith(".csv")) {
uploadFileName.value = uploadFile.name.slice(0, -4);
} else {
// Handle error or invalid file format
uploadFileName.value = ''; // Or other appropriate error handling
uploadFileName.value = ""; // Or other appropriate error handling
}
// Clear the selected file
if(fileInput) {
fileInput.value = '';
if (fileInput) {
fileInput.value = "";
}
}
onBeforeUnmount(() => {
emit('closeModal', false);
emit("closeModal", false);
});
</script>
<style scoped>

View File

@@ -1,19 +1,35 @@
<template>
<div id='header.vue' class="mx-auto px-4 h-14 z-50">
<div id="header.vue" class="mx-auto px-4 h-14 z-50">
<div class="flex justify-between items-center h-full">
<figure>
<DspLogo />
<DspLogo />
</figure>
<div class="flex justify-between items-center relative"
v-show="showMember">
<img id="acct_mgmt_button" v-if="!isHeadHovered" src="@/assets/icon-head-black.svg" @mouseenter='isHeadHovered = true'
width="32" height="32" @click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50" alt="user-head"
/>
<img id="acct_mgmt_button" v-else src="@/assets/icon-head-blue.svg" @mouseleave='isHeadHovered = false'
width="32" height="32" @click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50" alt="user-head"
/>
<div
class="flex justify-between items-center relative"
v-show="showMember"
>
<img
id="acct_mgmt_button"
v-if="!isHeadHovered"
src="@/assets/icon-head-black.svg"
@mouseenter="isHeadHovered = true"
width="32"
height="32"
@click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50"
alt="user-head"
/>
<img
id="acct_mgmt_button"
v-else
src="@/assets/icon-head-blue.svg"
@mouseleave="isHeadHovered = false"
width="32"
height="32"
@click="toggleIsAcctMenuOpen"
class="cursor-pointer z-50"
alt="user-head"
/>
</div>
</div>
</div>
@@ -31,16 +47,16 @@
* user account menu toggle button.
*/
import { ref, onMounted, } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs, } from 'pinia';
import emitter from '@/utils/emitter';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import DspLogo from '@/components/icons/DspLogo.vue';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { leaveFilter, leaveConformance } from '@/module/alertModal.js';
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import emitter from "@/utils/emitter";
import { useLoginStore } from "@/stores/login";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import DspLogo from "@/components/icons/DspLogo.vue";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { leaveFilter, leaveConformance } from "@/module/alertModal.js";
const route = useRoute();
@@ -49,8 +65,13 @@ const { logOut } = store;
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { tempFilterId, temporaryData, postRuleData, ruleData } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId, conformanceFileName } = storeToRefs(conformanceStore);
const { tempFilterId, temporaryData, postRuleData, ruleData } =
storeToRefs(allMapDataStore);
const {
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
conformanceFileName,
} = storeToRefs(conformanceStore);
const isHeadHovered = ref(false);
const showMember = ref(false);
@@ -64,21 +85,31 @@ const toggleIsAcctMenuOpen = () => {
* and Conformance pages.
*/
function logOutButton() {
if ((route.name === 'Map' || route.name === 'CheckMap') && tempFilterId.value) {
if (
(route.name === "Map" || route.name === "CheckMap") &&
tempFilterId.value
) {
// Notify Map to close the Sidebar.
emitter.emit('leaveFilter', false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut)
} else if((route.name === 'Conformance' || route.name === 'CheckConformance')
&& (conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)) {
leaveConformance(false, conformanceStore.addConformanceCreateCheckId, false, logOut)
emitter.emit("leaveFilter", false);
leaveFilter(false, allMapDataStore.addFilterId, false, logOut);
} else if (
(route.name === "Conformance" || route.name === "CheckConformance") &&
(conformanceLogTempCheckId.value || conformanceFilterTempCheckId.value)
) {
leaveConformance(
false,
conformanceStore.addConformanceCreateCheckId,
false,
logOut,
);
} else {
logOut();
}
}
onMounted(() => {
if (route.name === 'Login' || route.name === 'NotFound404') {
showMember.value = false
if (route.name === "Login" || route.name === "NotFound404") {
showMember.value = false;
} else {
showMember.value = true;
}

View File

@@ -3,7 +3,9 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<div class="w-full h-full fixed inset-0 m-auto flex justify-center items-center bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[9999]">
<div
class="w-full h-full fixed inset-0 m-auto flex justify-center items-center bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[9999]"
>
<span class="loader block"></span>
</div>
</template>

View File

@@ -1,32 +1,62 @@
<template>
<nav id='nav_bar' class="bg-neutral-700">
<div class="mx-auto px-4" :class="[showNavbarBreadcrumb? 'min-h-12': 'h-12']">
<div class="flex justify-between items-center flex-wrap relative" v-show="showNavbarBreadcrumb">
<nav id="nav_bar" class="bg-neutral-700">
<div
class="mx-auto px-4"
:class="[showNavbarBreadcrumb ? 'min-h-12' : 'h-12']"
>
<div
class="flex justify-between items-center flex-wrap relative"
v-show="showNavbarBreadcrumb"
>
<div id="nav_bar_logged_in" class="flex flex-1 items-center">
<!-- Back to Files page -->
<router-link to="/files" class="mr-4" v-if="showIcon" id="backPage">
<span class="material-symbols-outlined text-neutral-10 !leading-loose">
<span
class="material-symbols-outlined text-neutral-10 !leading-loose"
>
arrow_back
</span>
</router-link>
<div>
<h2 v-if="navViewName !== 'UPLOAD'" class="mr-14 py-3 text-2xl font-black text-neutral-10">{{ navViewName }}</h2>
<h2 v-else class="mr-14 py-3 text-2xl font-black text-neutral-10">FILES</h2>
<h2
v-if="navViewName !== 'UPLOAD'"
class="mr-14 py-3 text-2xl font-black text-neutral-10"
>
{{ navViewName }}
</h2>
<h2 v-else class="mr-14 py-3 text-2xl font-black text-neutral-10">
FILES
</h2>
</div>
<ul class="flex justify-center items-center space-x-4 text-xl font-semibold text-neutral-300 cursor-pointer">
<li @click="onNavItemBtnClick($event, item)"
<ul
class="flex justify-center items-center space-x-4 text-xl font-semibold text-neutral-300 cursor-pointer"
>
<li
@click="onNavItemBtnClick($event, item)"
v-for="(item, index) in navViewData[navViewName]"
:key="index" class="nav-item"
:class="{'active': activePage === item}">
:key="index"
class="nav-item"
:class="{ active: activePage === item }"
>
{{ item }}
</li>
</ul>
</div>
<!-- Files Page: Search and Upload -->
<div class="flex justify-end items-center" v-if="navViewName === 'FILES'">
<div id="import_btn" class="btn btn-sm btn-neutral cursor-pointer" @click="uploadModal = true">
<div
class="flex justify-end items-center"
v-if="navViewName === 'FILES'"
>
<div
id="import_btn"
class="btn btn-sm btn-neutral cursor-pointer"
@click="uploadModal = true"
>
Import
<UploadModal :visible="uploadModal" @closeModal="uploadModal = $event"></UploadModal>
<UploadModal
:visible="uploadModal"
@closeModal="uploadModal = $event"
></UploadModal>
</div>
</div>
<!-- Upload, Performance, Compare have no button actions -->
@@ -34,12 +64,16 @@
<!-- Other Page: Save and Download -->
<!-- Save: if data exists, prompt rename; if no data, prompt save; if unchanged, do nothing -->
<div v-else class="space-x-4">
<button class="btn btn-sm" :class="[ disabledSave ? 'btn-disable' : 'btn-neutral']"
:disabled="disabledSave" @click="saveModal">
<button
class="btn btn-sm"
:class="[disabledSave ? 'btn-disable' : 'btn-neutral']"
:disabled="disabledSave"
@click="saveModal"
>
Save
</button>
</div>
<AcctMenu v-if="showNavbarBreadcrumb"/>
<AcctMenu v-if="showNavbarBreadcrumb" />
</div>
</div>
</nav>
@@ -57,20 +91,24 @@
* Map/Conformance pages.
*/
import { ref, computed, watch, onMounted, } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { storeToRefs, } from 'pinia';
import emitter from '@/utils/emitter';
import { useFilesStore } from '@/stores/files';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useMapCompareStore } from '@/stores/mapCompareStore';
import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue';
import { saveFilter, savedSuccessfully, saveConformance } from '@/module/alertModal.js';
import UploadModal from './File/UploadModal.vue';
import AcctMenu from './AccountMenu/AcctMenu.vue';
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { storeToRefs } from "pinia";
import emitter from "@/utils/emitter";
import { useFilesStore } from "@/stores/files";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useMapCompareStore } from "@/stores/mapCompareStore";
import IconSearch from "@/components/icons/IconSearch.vue";
import IconSetting from "@/components/icons/IconSetting.vue";
import {
saveFilter,
savedSuccessfully,
saveConformance,
} from "@/module/alertModal.js";
import UploadModal from "./File/UploadModal.vue";
import AcctMenu from "./AccountMenu/AcctMenu.vue";
const route = useRoute();
const router = useRouter();
@@ -81,54 +119,87 @@ const conformanceStore = useConformanceStore();
const mapCompareStore = useMapCompareStore();
const pageAdminStore = usePageAdminStore();
const { logId, tempFilterId, createFilterId, filterName, postRuleData, isUpdateFilter } = storeToRefs(allMapDataStore);
const { conformanceRuleData, conformanceLogId, conformanceFilterId,
conformanceLogTempCheckId, conformanceFilterTempCheckId,
conformanceLogCreateCheckId, conformanceFilterCreateCheckId,
isUpdateConformance, conformanceFileName
const {
logId,
tempFilterId,
createFilterId,
filterName,
postRuleData,
isUpdateFilter,
} = storeToRefs(allMapDataStore);
const {
conformanceRuleData,
conformanceLogId,
conformanceFilterId,
conformanceLogTempCheckId,
conformanceFilterTempCheckId,
conformanceLogCreateCheckId,
conformanceFilterCreateCheckId,
isUpdateConformance,
conformanceFileName,
} = storeToRefs(conformanceStore);
const { activePage, pendingActivePage, activePageComputedByRoute, shouldKeepPreviousPage } = storeToRefs(pageAdminStore);
const { setPendingActivePage, setPreviousPage, setActivePage, setActivePageComputedByRoute, setIsPagePendingBoolean } = pageAdminStore;
const {
activePage,
pendingActivePage,
activePageComputedByRoute,
shouldKeepPreviousPage,
} = storeToRefs(pageAdminStore);
const {
setPendingActivePage,
setPreviousPage,
setActivePage,
setActivePageComputedByRoute,
setIsPagePendingBoolean,
} = pageAdminStore;
const showNavbarBreadcrumb = ref(false);
const navViewData = {
// e.g. FILES: ['ALL', 'DISCOVER', 'COMPARE', 'DESIGN', 'SIMULATION'],
FILES: ['ALL', 'DISCOVER', 'COMPARE'],
FILES: ["ALL", "DISCOVER", "COMPARE"],
// e.g. DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE'],
DISCOVER: ["MAP", "CONFORMANCE", "PERFORMANCE"],
// e.g. COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ['MAP', 'PERFORMANCE'],
'ACCOUNT MANAGEMENT': [],
'MY ACCOUNT': [],
COMPARE: ["MAP", "PERFORMANCE"],
"ACCOUNT MANAGEMENT": [],
"MY ACCOUNT": [],
};
const navViewName = ref('FILES');
const navViewName = ref("FILES");
const uploadModal = ref(false);
const disabledSave = computed(() => {
switch (route.name) {
case 'Map':
case 'CheckMap':
case "Map":
case "CheckMap":
// Cannot save without a filter ID or a temporary tempFilterId
return !tempFilterId.value;
case 'Conformance':
case 'CheckConformance':
return !(conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value);
case "Conformance":
case "CheckConformance":
return !(
conformanceFilterTempCheckId.value || conformanceLogTempCheckId.value
);
}
});
const showIcon = computed(() => {
return !['FILES', 'UPLOAD'].includes(navViewName.value);
return !["FILES", "UPLOAD"].includes(navViewName.value);
});
const noShowSaveButton = computed(() => {
return navViewName.value === 'UPLOAD' || navViewName.value === 'COMPARE' ||
navViewName.value === 'ACCOUNT MANAGEMENT' ||
activePage.value === 'PERFORMANCE';
return (
navViewName.value === "UPLOAD" ||
navViewName.value === "COMPARE" ||
navViewName.value === "ACCOUNT MANAGEMENT" ||
activePage.value === "PERFORMANCE"
);
});
watch(() => route, () => {
getNavViewName();
}, { deep: true });
watch(
() => route,
() => {
getNavViewName();
},
{ deep: true },
);
watch(filterName, (newVal) => {
filterName.value = newVal;
@@ -147,53 +218,76 @@ function onNavItemBtnClick(event) {
setPendingActivePage(navItemCandidate);
switch (navViewName.value) {
case 'FILES':
case "FILES":
store.filesTag = navItemCandidate;
break;
case 'DISCOVER':
case "DISCOVER":
type = route.params.type;
fileId = route.params.fileId;
isCheckPage = route.name.includes('Check');
isCheckPage = route.name.includes("Check");
switch (navItemCandidate) {
case 'MAP':
if(isCheckPage) {
router.push({name: 'CheckMap', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Map', params: { type: type, fileId: fileId }});
case "MAP":
if (isCheckPage) {
router.push({
name: "CheckMap",
params: { type: type, fileId: fileId },
});
} else {
router.push({
name: "Map",
params: { type: type, fileId: fileId },
});
}
break;
case 'CONFORMANCE':
if(isCheckPage) { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'CheckConformance', params: { type: type, fileId: fileId }});
case "CONFORMANCE":
if (isCheckPage) {
// Beware of Swal popup, it might disturb which is the current active page
router.push({
name: "CheckConformance",
params: { type: type, fileId: fileId },
});
} else {
// Beware of Swal popup, it might disturb which is the current active page
router.push({
name: "Conformance",
params: { type: type, fileId: fileId },
});
}
else { // Beware of Swal popup, it might disturb which is the current active page
router.push({name: 'Conformance', params: { type: type, fileId: fileId }});
}
break
case 'PERFORMANCE':
if(isCheckPage) {
router.push({name: 'CheckPerformance', params: { type: type, fileId: fileId }});
}
else {
router.push({name: 'Performance', params: { type: type, fileId: fileId }});
break;
case "PERFORMANCE":
if (isCheckPage) {
router.push({
name: "CheckPerformance",
params: { type: type, fileId: fileId },
});
} else {
router.push({
name: "Performance",
params: { type: type, fileId: fileId },
});
}
break;
}
break;
case 'COMPARE':
case "COMPARE":
switch (navItemCandidate) {
case 'MAP':
router.push({name: 'MapCompare', params: mapCompareStore.routeParam});
case "MAP":
router.push({
name: "MapCompare",
params: mapCompareStore.routeParam,
});
break;
case 'PERFORMANCE':
router.push({name: 'CompareDashboard', params: mapCompareStore.routeParam});
case "PERFORMANCE":
router.push({
name: "CompareDashboard",
params: mapCompareStore.routeParam,
});
break;
default:
break;
}
};
}
}
/**
@@ -201,42 +295,41 @@ function onNavItemBtnClick(event) {
* @returns {string} The navigation item name to highlight.
*/
function getNavViewName() {
const name = route.name;
let valueToSet;
if(route.name === 'NotFound404' || !route.matched[1]) {
if (route.name === "NotFound404" || !route.matched[1]) {
return;
}
// route.matched[1] is the second matched route record for the current route
navViewName.value = route.matched[1].name.toUpperCase();
store.filesTag = 'ALL';
store.filesTag = "ALL";
switch (navViewName.value) {
case 'FILES':
case "FILES":
valueToSet = activePage.value;
break;
case 'DISCOVER':
case "DISCOVER":
switch (name) {
case 'Map':
case 'CheckMap':
valueToSet = 'MAP';
case "Map":
case "CheckMap":
valueToSet = "MAP";
break;
case 'Conformance':
case 'CheckConformance':
valueToSet = 'CONFORMANCE';
case "Conformance":
case "CheckConformance":
valueToSet = "CONFORMANCE";
break;
case 'Performance':
case 'CheckPerformance':
valueToSet = 'PERFORMANCE';
case "Performance":
case "CheckPerformance":
valueToSet = "PERFORMANCE";
break;
}
break;
case 'COMPARE':
switch(name) {
case 'dummy':
case 'CompareDashboard':
valueToSet = 'DASHBOARD';
case "COMPARE":
switch (name) {
case "dummy":
case "CompareDashboard":
valueToSet = "DASHBOARD";
break;
default:
break;
@@ -248,11 +341,11 @@ function getNavViewName() {
// so here we need to save to a pending state
// The frontend cannot determine which modal button the user will press
// (cancel or confirm/save), so we save it to a pending state.
if(!shouldKeepPreviousPage.value) { // If the user did not press cancel
if (!shouldKeepPreviousPage.value) {
// If the user did not press cancel
setPendingActivePage(valueToSet);
}
return valueToSet;
}
@@ -260,27 +353,27 @@ function getNavViewName() {
async function saveModal() {
// Help determine MAP/CONFORMANCE save with "submit" or "cancel".
// Notify Map to close the Sidebar.
emitter.emit('saveModal', false);
emitter.emit("saveModal", false);
switch (route.name) {
case 'Map':
await handleMapSave();
break;
case 'CheckMap':
await handleCheckMapSave();
break;
case 'Conformance':
case 'CheckConformance':
await handleConformanceSave();
break;
default:
break;
}
case "Map":
await handleMapSave();
break;
case "CheckMap":
await handleCheckMapSave();
break;
case "Conformance":
case "CheckConformance":
await handleConformanceSave();
break;
default:
break;
}
}
/** Sets nav item button background color when the active page is empty. */
function handleNavItemBtn() {
if(activePageComputedByRoute.value === "") {
if (activePageComputedByRoute.value === "") {
setActivePageComputedByRoute(route.matched[route.matched.length - 1].name);
}
}
@@ -290,13 +383,13 @@ async function handleMapSave() {
if (createFilterId.value) {
await allMapDataStore.updateFilter();
if (isUpdateFilter.value) {
await savedSuccessfully(filterName.value);
}
await savedSuccessfully(filterName.value);
}
} else if (logId.value) {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
setActivePage("MAP");
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
}
@@ -305,43 +398,52 @@ async function handleMapSave() {
async function handleCheckMapSave() {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
setActivePage('MAP');
await router.push(`/discover/filter/${createFilterId.value}/map`);
setActivePage("MAP");
await router.push(`/discover/filter/${createFilterId.value}/map`);
}
}
/** Saves or updates conformance check data. */
async function handleConformanceSave() {
if (conformanceFilterCreateCheckId.value || conformanceLogCreateCheckId.value) {
await conformanceStore.updateConformance();
if (isUpdateConformance.value) {
await savedSuccessfully(conformanceFileName.value);
}
if (
conformanceFilterCreateCheckId.value ||
conformanceLogCreateCheckId.value
) {
await conformanceStore.updateConformance();
if (isUpdateConformance.value) {
await savedSuccessfully(conformanceFileName.value);
}
} else {
const isSaved = await saveConformance(conformanceStore.addConformanceCreateCheckId);
if (isSaved) {
if (conformanceLogId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`);
} else if (conformanceFilterId.value) {
setActivePage('CONFORMANCE');
await router.push(`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`);
}
const isSaved = await saveConformance(
conformanceStore.addConformanceCreateCheckId,
);
if (isSaved) {
if (conformanceLogId.value) {
setActivePage("CONFORMANCE");
await router.push(
`/discover/conformance/log/${conformanceLogCreateCheckId.value}/conformance`,
);
} else if (conformanceFilterId.value) {
setActivePage("CONFORMANCE");
await router.push(
`/discover/conformance/filter/${conformanceFilterCreateCheckId.value}/conformance`,
);
}
}
}
}
onMounted(() => {
handleNavItemBtn();
if(route.params.type === 'filter') {
if (route.params.type === "filter") {
createFilterId.value = route.params.fileId;
}
showNavbarBreadcrumb.value = route.matched[0].name !== ('AuthContainer');
showNavbarBreadcrumb.value = route.matched[0].name !== "AuthContainer";
getNavViewName();
});
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{
#searchFiles::-webkit-search-cancel-button {
appearance: none;
}
</style>

View File

@@ -2,12 +2,19 @@
<form role="search">
<label for="searchFiles" class="mr-4 relative" htmlFor="searchFiles">
Search
<input type="search" id="searchFiles" placeholder="Search Activity" class="px-5 py-2 w-52 rounded-full text-sm align-middle
duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring
focus:border-neutral-500"/>
<span class="absolute top-2 bottom-0.5 right-0.5 flex justify-center items-center gap-2">
<input
type="search"
id="searchFiles"
placeholder="Search Activity"
class="px-5 py-2 w-52 rounded-full text-sm align-middle duration-300 border bg-neutral-100 border-neutral-300 hover:border-neutral-500 focus:outline-none focus:ring focus:border-neutral-500"
/>
<span
class="absolute top-2 bottom-0.5 right-0.5 flex justify-center items-center gap-2"
>
<IconSetting class="w-6 h-6 cursor-pointer"></IconSetting>
<span class="w-px h-6 block after:border after:border-neutral-300 after:content-['']"></span>
<span
class="w-px h-6 block after:border after:border-neutral-300 after:content-['']"
></span>
<button class="pr-2">
<IconSearch class="w-6 h-6"></IconSearch>
</button>
@@ -28,6 +35,6 @@
* search input, settings icon, and search icon button.
*/
import IconSearch from '@/components/icons/IconSearch.vue';
import IconSetting from '@/components/icons/IconSetting.vue';
import IconSearch from "@/components/icons/IconSearch.vue";
import IconSetting from "@/components/icons/IconSetting.vue";
</script>

View File

@@ -1,42 +1,52 @@
<template>
<div class="relative">
<div class="w-32 px-1 border border-neutral-500 cursor-pointer" @click="openTimeSelect = !openTimeSelect" :id="size">
<div v-if="totalSeconds === 0" class="text-center">
<p>0</p>
<div class="relative">
<div
class="w-32 px-1 border border-neutral-500 cursor-pointer"
@click="openTimeSelect = !openTimeSelect"
:id="size"
>
<div v-if="totalSeconds === 0" class="text-center">
<p>0</p>
</div>
<!-- This section shows the fixed time display, not the popup -->
<div
id="cyp-timerange-show"
v-else
class="flex justify-center items-center gap-1"
>
<p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p>
<p v-show="minutes != 0">{{ minutes }}m</p>
<p v-show="seconds != 0">{{ seconds }}s</p>
</div>
</div>
<!-- This section shows the fixed time display, not the popup -->
<div id="cyp-timerange-show" v-else class="flex justify-center items-center gap-1">
<p v-show="days != 0">{{ days }}d</p>
<p v-show="hours != 0">{{ hours }}h</p>
<p v-show="minutes != 0">{{ minutes }}m</p>
<p v-show="seconds != 0">{{ seconds }}s</p>
<!-- The following section is the popup that appears when the user clicks to open -->
<div
class="duration-container absolute left-0 top-full translate-y-2 dhms-input-popup-container"
v-show="openTimeSelect"
v-closable="{ id: size, handler: onClose }"
>
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
<input
id="input_duration_dhms"
type="text"
class="duration duration-val input-dhms-field"
:data-index="index"
:data-tunit="unit"
:data-max="tUnits[unit].max"
:data-min="tUnits[unit].min"
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
@focus="onFocus"
@change="onChange"
@keyup="onKeyUp"
v-model="inputTimeFields[index]"
/>
<label class="duration" for="input_duration_dhms">{{
tUnits[unit].dsp
}}</label>
</div>
</div>
</div>
<!-- The following section is the popup that appears when the user clicks to open -->
<div class="duration-container absolute left-0 top-full translate-y-2
dhms-input-popup-container"
v-show="openTimeSelect"
v-closable="{id: size, handler: onClose}">
<div class="duration-box" v-for="(unit, index) in inputTypes" :key="unit">
<input
id="input_duration_dhms"
type="text"
class="duration duration-val input-dhms-field"
:data-index="index"
:data-tunit="unit"
:data-max="tUnits[unit].max"
:data-min="tUnits[unit].min"
:maxlength="tUnits[unit].dsp === 'd' ? 3 : 2"
@focus="onFocus"
@change="onChange"
@keyup="onKeyUp"
v-model="inputTimeFields[index]"
/>
<label class="duration" for="input_duration_dhms">{{ tUnits[unit].dsp }}</label>
</div>
</div>
</div>
</template>
<script setup>
@@ -52,8 +62,8 @@
* fields and bounded min/max validation.
*/
import { ref, computed, watch, onMounted } from 'vue';
import emitter from '@/utils/emitter';
import { ref, computed, watch, onMounted } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps({
max: {
@@ -97,12 +107,12 @@ const props = defineProps({
validator(value) {
return value >= 0;
},
}
},
});
const emit = defineEmits(['total-seconds']);
const emit = defineEmits(["total-seconds"]);
const display = ref('dhms');
const display = ref("dhms");
const seconds = ref(0);
const minutes = ref(0);
const hours = ref(0);
@@ -119,19 +129,34 @@ const openTimeSelect = ref(false);
const tUnits = computed({
get() {
return {
s: { dsp: 's', inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: 'm', inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: 'h', inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: { dsp: 'd', inc: 1, val: days.value, max: maxDays.value, rate: 86400, min: minDays.value }
s: { dsp: "s", inc: 1, val: seconds.value, max: 59, rate: 1, min: 0 },
m: { dsp: "m", inc: 1, val: minutes.value, max: 59, rate: 60, min: 0 },
h: { dsp: "h", inc: 1, val: hours.value, max: 23, rate: 3600, min: 0 },
d: {
dsp: "d",
inc: 1,
val: days.value,
max: maxDays.value,
rate: 86400,
min: minDays.value,
},
};
},
set(newValues) {
for (const unit in newValues) {
switch (unit) {
case 's': seconds.value = newValues[unit].val; break;
case 'm': minutes.value = newValues[unit].val; break;
case 'h': hours.value = newValues[unit].val; break;
case 'd': days.value = newValues[unit].val; break;
case "s":
seconds.value = newValues[unit].val;
break;
case "m":
minutes.value = newValues[unit].val;
break;
case "h":
hours.value = newValues[unit].val;
break;
case "d":
days.value = newValues[unit].val;
break;
}
const input = document.querySelector(`[data-tunit="${unit}"]`);
if (input) {
@@ -143,8 +168,10 @@ const tUnits = computed({
const inputTimeFields = computed(() => {
const paddedTimeFields = [];
inputTypes.value.forEach(inputTypeUnit => {
paddedTimeFields.push(tUnits.value[inputTypeUnit].val.toString().padStart(2, '0'));
inputTypes.value.forEach((inputTypeUnit) => {
paddedTimeFields.push(
tUnits.value[inputTypeUnit].val.toString().padStart(2, "0"),
);
});
return paddedTimeFields;
});
@@ -170,8 +197,8 @@ function onFocus(event) {
function onChange(event) {
const baseInputValue = event.target.value;
let decoratedInputValue;
if(isNaN(event.target.value)){
event.target.value = '00';
if (isNaN(event.target.value)) {
event.target.value = "00";
} else {
event.target.value = event.target.value.toString();
}
@@ -180,27 +207,27 @@ function onChange(event) {
const inputValue = parseInt(event.target.value, 10);
const max = parseInt(event.target.dataset.max, 10);
const min = parseInt(event.target.dataset.min, 10);
if(inputValue > max) {
decoratedInputValue = max.toString().padStart(2, '0');
}else if(inputValue < min) {
decoratedInputValue= min.toString();
if (inputValue > max) {
decoratedInputValue = max.toString().padStart(2, "0");
} else if (inputValue < min) {
decoratedInputValue = min.toString();
}
const dsp = event.target.dataset.tunit;
tUnits.value[dsp].val = decoratedInputValue;
switch (dsp) {
case 'd':
case "d":
days.value = baseInputValue;
break;
case 'h':
case "h":
hours.value = decoratedInputValue;
break;
case 'm':
case "m":
minutes.value = decoratedInputValue;
break;
case 's':
case "s":
seconds.value = decoratedInputValue;
break;
};
}
calculateTotalSeconds();
}
@@ -210,10 +237,10 @@ function onChange(event) {
* @param {KeyboardEvent} event - The keyup event.
*/
function onKeyUp(event) {
event.target.value = event.target.value.replace(/\D/g, '');
event.target.value = event.target.value.replace(/\D/g, "");
if (event.keyCode === 38 || event.keyCode === 40) {
actionUpDown(event.target, event.keyCode === 38, true);
};
}
}
/**
@@ -247,7 +274,7 @@ function getNewValue(input) {
function handleArrowUp(newVal, tUnit, input) {
newVal += tUnits.value[tUnit].inc;
if (newVal > tUnits.value[tUnit].max) {
if (tUnits.value[tUnit].dsp === 'd') {
if (tUnits.value[tUnit].dsp === "d") {
totalSeconds.value = maxTotal.value;
} else {
newVal = newVal % (tUnits.value[tUnit].max + 1);
@@ -260,14 +287,16 @@ function handleArrowUp(newVal, tUnit, input) {
function handleArrowDown(newVal, tUnit) {
newVal -= tUnits.value[tUnit].inc;
if (newVal < 0) {
newVal = (tUnits.value[tUnit].max + 1) - tUnits.value[tUnit].inc;
newVal = tUnits.value[tUnit].max + 1 - tUnits.value[tUnit].inc;
}
return newVal;
}
function incrementPreviousUnit(input) {
if (input.dataset.index > 0) {
const prevUnit = document.querySelector(`input[data-index="${parseInt(input.dataset.index) - 1}"]`);
const prevUnit = document.querySelector(
`input[data-index="${parseInt(input.dataset.index) - 1}"]`,
);
actionUpDown(prevUnit, true);
}
}
@@ -275,16 +304,16 @@ function incrementPreviousUnit(input) {
function updateInputValue(input, newVal, tUnit) {
input.value = newVal.toString();
switch (tUnit) {
case 'd':
case "d":
days.value = input.value;
break;
case 'h':
case "h":
hours.value = input.value;
break;
case 'm':
case "m":
minutes.value = input.value;
break;
case 's':
case "s":
seconds.value = input.value;
break;
}
@@ -297,19 +326,18 @@ function updateInputValue(input, newVal, tUnit) {
*/
function secondToDate(totalSec, size) {
totalSec = parseInt(totalSec);
if(!isNaN(totalSec)) {
if (!isNaN(totalSec)) {
seconds.value = totalSec % 60;
minutes.value = (Math.floor(totalSec - seconds.value) / 60) % 60;
hours.value = (Math.floor(totalSec / 3600)) % 24;
hours.value = Math.floor(totalSec / 3600) % 24;
days.value = Math.floor(totalSec / (3600 * 24));
if(size === 'max') {
if (size === "max") {
maxDays.value = Math.floor(totalSec / (3600 * 24));
}
else if(size === 'min') {
} else if (size === "min") {
minDays.value = Math.floor(totalSec / (3600 * 24));
}
};
}
}
/** Calculates total seconds from all duration units and emits the result. */
@@ -323,39 +351,39 @@ function calculateTotalSeconds() {
}
}
if(total >= maxTotal.value){
if (total >= maxTotal.value) {
total = maxTotal.value;
secondToDate(maxTotal.value, 'max');
secondToDate(maxTotal.value, "max");
} else if (total <= minTotal.value) {
total = minTotal.value;
secondToDate(minTotal.value, 'min');
} else if((props.size === 'min' && total <= maxTotal.value)) {
secondToDate(minTotal.value, "min");
} else if (props.size === "min" && total <= maxTotal.value) {
maxDays.value = Math.floor(maxTotal.value / (3600 * 24));
}
totalSeconds.value = total;
emit('total-seconds', total);
emit("total-seconds", total);
}
/** Initializes the duration display based on min/max boundaries and preset value. */
async function createData() {
const size = props.size;
if (maxTotal.value !== await null && minTotal.value !== await null) {
if (maxTotal.value !== (await null) && minTotal.value !== (await null)) {
switch (size) {
case 'max':
secondToDate(minTotal.value, 'min');
secondToDate(maxTotal.value, 'max');
case "max":
secondToDate(minTotal.value, "min");
secondToDate(maxTotal.value, "max");
totalSeconds.value = maxTotal.value;
if(props.value !== null) {
if (props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
break;
case 'min':
secondToDate(maxTotal.value, 'max');
secondToDate(minTotal.value, 'min');
case "min":
secondToDate(maxTotal.value, "max");
secondToDate(minTotal.value, "min");
totalSeconds.value = minTotal.value;
if(props.value !== null) {
if (props.value !== null) {
totalSeconds.value = props.value;
secondToDate(props.value);
}
@@ -365,33 +393,33 @@ async function createData() {
}
// created
emitter.on('reset', () => {
emitter.on("reset", () => {
createData();
});
// mounted
onMounted(() => {
inputTypes.value = display.value.split('');
inputTypes.value = display.value.split("");
});
const vClosable = {
mounted(el, {value}) {
const handleOutsideClick = function(e) {
mounted(el, { value }) {
const handleOutsideClick = function (e) {
let target = e.target;
while (target && target.id !== value.id) {
target = target.parentElement;
};
const isClickOutside = target?.id !== value.id && !el.contains(e.target)
}
const isClickOutside = target?.id !== value.id && !el.contains(e.target);
if (isClickOutside) {
value.handler();
}
e.stopPropagation();
}
document.addEventListener('click', handleOutsideClick);
};
document.addEventListener("click", handleOutsideClick);
el._handleOutsideClick = handleOutsideClick;
},
unmounted(el) {
document.removeEventListener('click', el._handleOutsideClick);
document.removeEventListener("click", el._handleOutsideClick);
},
};
</script>
@@ -399,55 +427,55 @@ const vClosable = {
.duration-container {
margin: 4px;
border-radius: 4px;
background: var(--10, #FFF);
background: var(--10, #fff);
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25);
height: 36px;
width: 221px;
}
.duration-box {
float:left;
overflow: auto;
height: var(--main-input-height);
padding: 4px;
float: left;
overflow: auto;
height: var(--main-input-height);
padding: 4px;
}
.duration-box > .duration {
border: 1px solid var(--main-bg-light);
border-right: 0;
border-left: 0;
background-color:transparent;
color: var(--main-bg-light);
border: 1px solid var(--main-bg-light);
border-right: 0;
border-left: 0;
background-color: transparent;
color: var(--main-bg-light);
}
.duration-box > .duration:nth-child(1) {
border-left: 1px solid var(--main-bg-light);
border-top-left-radius: var(--main-input-br);
border-bottom-left-radius: var(--main-input-br);
border-left: 1px solid var(--main-bg-light);
border-top-left-radius: var(--main-input-br);
border-bottom-left-radius: var(--main-input-br);
}
.duration-box > .duration:nth-last-child(1) {
border-right: 1px solid var(--main-bg-light);
border-top-right-radius: var(--main-input-br);
border-bottom-right-radius: var(--main-input-br);
border-right: 1px solid var(--main-bg-light);
border-top-right-radius: var(--main-input-br);
border-bottom-right-radius: var(--main-input-br);
}
.duration {
float:left;
display: block;
overflow: auto;
height: var(--main-input-height);
outline: none;
font-size: 14px;
margin: 0px 2px;
float: left;
display: block;
overflow: auto;
height: var(--main-input-height);
outline: none;
font-size: 14px;
margin: 0px 2px;
}
.duration-box > label.duration {
line-height: 28px;
width: 12px;
overflow: hidden;
line-height: 28px;
width: 12px;
overflow: hidden;
}
.duration-box > input[type="text"].duration {
text-align: right;
width: 26px;
padding: 3px 2px 0px 0px;
text-align: right;
width: 26px;
padding: 3px 2px 0px 0px;
}
.duration-box > input[type="button"].duration {
width: 60px;
cursor: pointer;
width: 60px;
cursor: pointer;
}
</style>

View File

@@ -1,11 +1,27 @@
<template>
<div
class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"> <!-- A relative div containing two absolutely positioned img elements -->
<img v-if="!isChecked" :src="ImgCheckboxGrayFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxBlueFrame" class="absolute" alt="checkbox"/>
<img v-if="isChecked" :src="ImgCheckboxCheckedMark" class="absolute top-[11x] left-[2px] h-[16px] w-[14px]" alt="checkbox"/>
</div>
<div
class="relative two-imgs-container w-[18px] h-[24px] cursor-pointer mt-1 mr-2"
>
<!-- A relative div containing two absolutely positioned img elements -->
<img
v-if="!isChecked"
:src="ImgCheckboxGrayFrame"
class="absolute"
alt="checkbox"
/>
<img
v-if="isChecked"
:src="ImgCheckboxBlueFrame"
class="absolute"
alt="checkbox"
/>
<img
v-if="isChecked"
:src="ImgCheckboxCheckedMark"
class="absolute top-[11x] left-[2px] h-[16px] w-[14px]"
alt="checkbox"
/>
</div>
</template>
<script setup>
@@ -24,9 +40,9 @@ import ImgCheckboxCheckedMark from "@/assets/icon-checkbox-checked.svg";
import ImgCheckboxGrayFrame from "@/assets/icon-checkbox-empty.svg";
defineProps({
isChecked: {
type: Boolean,
required: true,
},
isChecked: {
type: Boolean,
required: true,
},
});
</script>

View File

@@ -3,10 +3,24 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M28.8571 31H3.14286C2.57471 30.9994 2.03 30.7735 1.62825 30.3717C1.22651 29.97 1.00057 29.4253 1 28.8571V3.14286C1.00057 2.57471 1.22651 2.03 1.62825 1.62825C2.03 1.22651 2.57471 1.00057 3.14286 1H28.8571C29.4253 1.00057 29.97 1.22651 30.3717 1.62825C30.7735 2.03 30.9994 2.57471 31 3.14286V28.8571C30.9994 29.4253 30.7735 29.97 30.3717 30.3717C29.97 30.7735 29.4253 30.9994 28.8571 31ZM3.14286 3.14286V28.8571H28.8571V3.14286H3.14286Z"/>
<path d="M11.1092 19.1541V20.2689H7.01401V19.1541H11.1092ZM7.40616 12.112V20.2689H6V12.112H7.40616Z"/>
<path d="M18.3305 15.9664V16.4146C18.3305 17.0308 18.2502 17.5836 18.0896 18.0728C17.929 18.5621 17.6993 18.9785 17.4006 19.3221C17.1055 19.6657 16.7507 19.929 16.3361 20.112C15.9216 20.2913 15.4622 20.381 14.958 20.381C14.4575 20.381 14 20.2913 13.5854 20.112C13.1746 19.929 12.8179 19.6657 12.5154 19.3221C12.2129 18.9785 11.9776 18.5621 11.8095 18.0728C11.6452 17.5836 11.563 17.0308 11.563 16.4146V15.9664C11.563 15.3501 11.6452 14.7993 11.8095 14.3137C11.9739 13.8245 12.2054 13.408 12.5042 13.0644C12.8067 12.7171 13.1634 12.4538 13.5742 12.2745C13.9888 12.0915 14.4463 12 14.9468 12C15.451 12 15.9104 12.0915 16.3249 12.2745C16.7395 12.4538 17.0962 12.7171 17.395 13.0644C17.6937 13.408 17.9234 13.8245 18.084 14.3137C18.2484 14.7993 18.3305 15.3501 18.3305 15.9664ZM16.9244 16.4146V15.9552C16.9244 15.4995 16.8796 15.098 16.7899 14.7507C16.704 14.3996 16.5752 14.1064 16.4034 13.8711C16.2353 13.6321 16.028 13.4528 15.7815 13.3333C15.535 13.2101 15.2568 13.1485 14.9468 13.1485C14.6368 13.1485 14.3604 13.2101 14.1176 13.3333C13.8749 13.4528 13.6676 13.6321 13.4958 13.8711C13.3277 14.1064 13.1989 14.3996 13.1092 14.7507C13.0196 15.098 12.9748 15.4995 12.9748 15.9552V16.4146C12.9748 16.8702 13.0196 17.2736 13.1092 17.6247C13.1989 17.9757 13.3296 18.2726 13.5014 18.5154C13.6769 18.7544 13.8861 18.9356 14.1289 19.0588C14.3716 19.1783 14.648 19.2381 14.958 19.2381C15.2717 19.2381 15.55 19.1783 15.7927 19.0588C16.0355 18.9356 16.2409 18.7544 16.409 18.5154C16.577 18.2726 16.704 17.9757 16.7899 17.6247C16.8796 17.2736 16.9244 16.8702 16.9244 16.4146Z"/>
<path d="M26 16.1008V19.2157C25.8842 19.3688 25.7031 19.5369 25.4566 19.7199C25.2138 19.8992 24.8908 20.0542 24.4874 20.1849C24.084 20.3156 23.5817 20.381 22.9804 20.381C22.4687 20.381 22 20.2951 21.5742 20.1233C21.1485 19.9477 20.7806 19.6919 20.4706 19.3557C20.1643 19.0196 19.9272 18.6106 19.7591 18.1289C19.591 17.6433 19.507 17.0906 19.507 16.4706V15.9048C19.507 15.2885 19.5836 14.7395 19.7367 14.2577C19.8936 13.7722 20.1176 13.3613 20.409 13.0252C20.7003 12.6891 21.0514 12.4351 21.4622 12.2633C21.8768 12.0878 22.3455 12 22.8683 12C23.5369 12 24.0896 12.112 24.5266 12.3361C24.9673 12.5565 25.3072 12.8627 25.5462 13.2549C25.7852 13.6471 25.9365 14.0952 26 14.5994H24.6218C24.577 14.3156 24.4893 14.0616 24.3585 13.8375C24.2316 13.6134 24.0486 13.4379 23.8095 13.3109C23.5742 13.1802 23.268 13.1148 22.8908 13.1148C22.5658 13.1148 22.2801 13.1765 22.0336 13.2997C21.7871 13.423 21.5817 13.6041 21.4174 13.8431C21.2568 14.0822 21.1354 14.3735 21.0532 14.7171C20.9711 15.0607 20.93 15.4528 20.93 15.8936V16.4706C20.93 16.9188 20.9767 17.3165 21.07 17.6639C21.1671 18.0112 21.3053 18.3044 21.4846 18.5434C21.6676 18.7824 21.8898 18.9636 22.1513 19.0868C22.4127 19.2063 22.7078 19.2661 23.0364 19.2661C23.3576 19.2661 23.6209 19.24 23.8263 19.1877C24.0317 19.1317 24.1942 19.0663 24.3137 18.9916C24.437 18.9132 24.5322 18.8385 24.5994 18.7675V17.1485H22.902V16.1008H26Z"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M28.8571 31H3.14286C2.57471 30.9994 2.03 30.7735 1.62825 30.3717C1.22651 29.97 1.00057 29.4253 1 28.8571V3.14286C1.00057 2.57471 1.22651 2.03 1.62825 1.62825C2.03 1.22651 2.57471 1.00057 3.14286 1H28.8571C29.4253 1.00057 29.97 1.22651 30.3717 1.62825C30.7735 2.03 30.9994 2.57471 31 3.14286V28.8571C30.9994 29.4253 30.7735 29.97 30.3717 30.3717C29.97 30.7735 29.4253 30.9994 28.8571 31ZM3.14286 3.14286V28.8571H28.8571V3.14286H3.14286Z"
/>
<path
d="M11.1092 19.1541V20.2689H7.01401V19.1541H11.1092ZM7.40616 12.112V20.2689H6V12.112H7.40616Z"
/>
<path
d="M18.3305 15.9664V16.4146C18.3305 17.0308 18.2502 17.5836 18.0896 18.0728C17.929 18.5621 17.6993 18.9785 17.4006 19.3221C17.1055 19.6657 16.7507 19.929 16.3361 20.112C15.9216 20.2913 15.4622 20.381 14.958 20.381C14.4575 20.381 14 20.2913 13.5854 20.112C13.1746 19.929 12.8179 19.6657 12.5154 19.3221C12.2129 18.9785 11.9776 18.5621 11.8095 18.0728C11.6452 17.5836 11.563 17.0308 11.563 16.4146V15.9664C11.563 15.3501 11.6452 14.7993 11.8095 14.3137C11.9739 13.8245 12.2054 13.408 12.5042 13.0644C12.8067 12.7171 13.1634 12.4538 13.5742 12.2745C13.9888 12.0915 14.4463 12 14.9468 12C15.451 12 15.9104 12.0915 16.3249 12.2745C16.7395 12.4538 17.0962 12.7171 17.395 13.0644C17.6937 13.408 17.9234 13.8245 18.084 14.3137C18.2484 14.7993 18.3305 15.3501 18.3305 15.9664ZM16.9244 16.4146V15.9552C16.9244 15.4995 16.8796 15.098 16.7899 14.7507C16.704 14.3996 16.5752 14.1064 16.4034 13.8711C16.2353 13.6321 16.028 13.4528 15.7815 13.3333C15.535 13.2101 15.2568 13.1485 14.9468 13.1485C14.6368 13.1485 14.3604 13.2101 14.1176 13.3333C13.8749 13.4528 13.6676 13.6321 13.4958 13.8711C13.3277 14.1064 13.1989 14.3996 13.1092 14.7507C13.0196 15.098 12.9748 15.4995 12.9748 15.9552V16.4146C12.9748 16.8702 13.0196 17.2736 13.1092 17.6247C13.1989 17.9757 13.3296 18.2726 13.5014 18.5154C13.6769 18.7544 13.8861 18.9356 14.1289 19.0588C14.3716 19.1783 14.648 19.2381 14.958 19.2381C15.2717 19.2381 15.55 19.1783 15.7927 19.0588C16.0355 18.9356 16.2409 18.7544 16.409 18.5154C16.577 18.2726 16.704 17.9757 16.7899 17.6247C16.8796 17.2736 16.9244 16.8702 16.9244 16.4146Z"
/>
<path
d="M26 16.1008V19.2157C25.8842 19.3688 25.7031 19.5369 25.4566 19.7199C25.2138 19.8992 24.8908 20.0542 24.4874 20.1849C24.084 20.3156 23.5817 20.381 22.9804 20.381C22.4687 20.381 22 20.2951 21.5742 20.1233C21.1485 19.9477 20.7806 19.6919 20.4706 19.3557C20.1643 19.0196 19.9272 18.6106 19.7591 18.1289C19.591 17.6433 19.507 17.0906 19.507 16.4706V15.9048C19.507 15.2885 19.5836 14.7395 19.7367 14.2577C19.8936 13.7722 20.1176 13.3613 20.409 13.0252C20.7003 12.6891 21.0514 12.4351 21.4622 12.2633C21.8768 12.0878 22.3455 12 22.8683 12C23.5369 12 24.0896 12.112 24.5266 12.3361C24.9673 12.5565 25.3072 12.8627 25.5462 13.2549C25.7852 13.6471 25.9365 14.0952 26 14.5994H24.6218C24.577 14.3156 24.4893 14.0616 24.3585 13.8375C24.2316 13.6134 24.0486 13.4379 23.8095 13.3109C23.5742 13.1802 23.268 13.1148 22.8908 13.1148C22.5658 13.1148 22.2801 13.1765 22.0336 13.2997C21.7871 13.423 21.5817 13.6041 21.4174 13.8431C21.2568 14.0822 21.1354 14.3735 21.0532 14.7171C20.9711 15.0607 20.93 15.4528 20.93 15.8936V16.4706C20.93 16.9188 20.9767 17.3165 21.07 17.6639C21.1671 18.0112 21.3053 18.3044 21.4846 18.5434C21.6676 18.7824 21.8898 18.9636 22.1513 19.0868C22.4127 19.2063 22.7078 19.2661 23.0364 19.2661C23.3576 19.2661 23.6209 19.24 23.8263 19.1877C24.0317 19.1317 24.1942 19.0663 24.3137 18.9916C24.437 18.9132 24.5322 18.8385 24.5994 18.7675V17.1485H22.902V16.1008H26Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.0529 3.24373C4.9174 3.10198 4.73177 3.01893 4.53578 3.01238C4.33979 3.00583 4.14903 3.07631 4.00437 3.20871C3.85971 3.34111 3.77266 3.5249 3.76187 3.7207C3.75108 3.9165 3.81741 4.10874 3.94665 4.25624L5.74665 6.23436C2.34352 8.32498 0.881025 11.55 0.8154 11.7C0.773866 11.7962 0.752441 11.8999 0.752441 12.0047C0.752441 12.1095 0.773866 12.2132 0.8154 12.3094C0.843525 12.375 1.6404 14.1375 3.4029 15.9094C5.75602 18.2531 8.7279 19.5 11.9998 19.5C13.6807 19.5067 15.3444 19.1618 16.8842 18.4875L18.9467 20.7562C19.0169 20.833 19.1023 20.8943 19.1976 20.9363C19.2928 20.9783 19.3957 21 19.4998 21C19.6869 20.9982 19.8669 20.9282 20.006 20.8031C20.0797 20.7373 20.1396 20.6574 20.1819 20.5681C20.2243 20.4789 20.2484 20.382 20.2528 20.2833C20.2571 20.1846 20.2417 20.086 20.2074 19.9933C20.173 19.9007 20.1205 19.8158 20.0529 19.7437L5.0529 3.24373ZM9.48727 10.3594L13.3966 14.6531C12.9666 14.8818 12.4868 15.0009 11.9998 15C11.4591 15.0001 10.9284 14.8542 10.4639 14.5775C9.99933 14.3009 9.61819 13.9038 9.36076 13.4283C9.10334 12.9528 8.97919 12.4166 9.00146 11.8764C9.02373 11.3362 9.19159 10.812 9.48727 10.3594V10.3594ZM11.9998 18C9.11228 18 6.5904 16.95 4.50915 14.8781C3.64691 14.0329 2.91685 13.0626 2.34352 12C2.78415 11.175 4.19978 8.85936 6.7779 7.36873L8.4654 9.22499C7.81494 10.0615 7.48054 11.1007 7.52112 12.1596C7.5617 13.2185 7.97465 14.2291 8.68723 15.0134C9.39981 15.7976 10.3663 16.3053 11.4164 16.4469C12.4666 16.5885 13.533 16.355 14.4279 15.7875L15.806 17.3062C14.5911 17.771 13.3005 18.0063 11.9998 18V18ZM23.1842 12.3094C23.1467 12.3937 22.1998 14.4937 20.0529 16.4156C19.9152 16.5359 19.7388 16.6025 19.556 16.6031C19.4509 16.6044 19.3467 16.5823 19.2511 16.5385C19.1555 16.4948 19.0707 16.4304 19.0029 16.35C18.937 16.2767 18.8862 16.1912 18.8534 16.0983C18.8205 16.0055 18.8063 15.907 18.8116 15.8086C18.8168 15.7103 18.8413 15.6139 18.8838 15.525C18.9263 15.4361 18.9859 15.3565 19.0592 15.2906C20.107 14.3507 20.9854 13.2376 21.656 12C21.0811 10.9356 20.3513 9.96244 19.4904 9.11248C17.4091 7.04998 14.8873 5.99999 11.9998 5.99999C11.3903 5.99753 10.7818 6.04772 10.181 6.14998C9.98493 6.17994 9.78489 6.13197 9.62371 6.01634C9.46252 5.90071 9.35299 5.72659 9.31852 5.53124C9.30244 5.43397 9.30569 5.33448 9.32809 5.23847C9.35049 5.14246 9.3916 5.0518 9.44907 4.9717C9.50654 4.89159 9.57924 4.8236 9.66301 4.77161C9.74678 4.71963 9.83998 4.68467 9.93727 4.66873C10.6189 4.55517 11.3088 4.49873 11.9998 4.49998C15.2717 4.49998 18.2435 5.74686 20.5966 8.09061C22.3591 9.86249 23.156 11.625 23.1842 11.7C23.2257 11.7962 23.2471 11.8999 23.2471 12.0047C23.2471 12.1095 23.2257 12.2132 23.1842 12.3094V12.3094ZM12.5623 9.05624C12.465 9.03777 12.3723 9.00032 12.2896 8.94604C12.2068 8.89176 12.1355 8.82171 12.0798 8.73987C11.9672 8.57461 11.925 8.37141 11.9623 8.17498C11.9996 7.97856 12.1134 7.80499 12.2786 7.69247C12.4439 7.57995 12.6471 7.53769 12.8435 7.57499C13.7999 7.76202 14.6703 8.25257 15.3257 8.97375C15.981 9.69494 16.3862 10.6083 16.481 11.5781C16.4993 11.7757 16.4385 11.9725 16.312 12.1254C16.1855 12.2782 16.0035 12.3747 15.806 12.3937H15.731C15.5455 12.3945 15.3664 12.3255 15.2292 12.2005C15.0921 12.0755 15.0068 11.9036 14.9904 11.7187C14.9257 11.0729 14.6546 10.4651 14.2172 9.98546C13.7798 9.50585 13.1995 9.18 12.5623 9.05624V9.05624Z" fill="#C4C4C4"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.0529 3.24373C4.9174 3.10198 4.73177 3.01893 4.53578 3.01238C4.33979 3.00583 4.14903 3.07631 4.00437 3.20871C3.85971 3.34111 3.77266 3.5249 3.76187 3.7207C3.75108 3.9165 3.81741 4.10874 3.94665 4.25624L5.74665 6.23436C2.34352 8.32498 0.881025 11.55 0.8154 11.7C0.773866 11.7962 0.752441 11.8999 0.752441 12.0047C0.752441 12.1095 0.773866 12.2132 0.8154 12.3094C0.843525 12.375 1.6404 14.1375 3.4029 15.9094C5.75602 18.2531 8.7279 19.5 11.9998 19.5C13.6807 19.5067 15.3444 19.1618 16.8842 18.4875L18.9467 20.7562C19.0169 20.833 19.1023 20.8943 19.1976 20.9363C19.2928 20.9783 19.3957 21 19.4998 21C19.6869 20.9982 19.8669 20.9282 20.006 20.8031C20.0797 20.7373 20.1396 20.6574 20.1819 20.5681C20.2243 20.4789 20.2484 20.382 20.2528 20.2833C20.2571 20.1846 20.2417 20.086 20.2074 19.9933C20.173 19.9007 20.1205 19.8158 20.0529 19.7437L5.0529 3.24373ZM9.48727 10.3594L13.3966 14.6531C12.9666 14.8818 12.4868 15.0009 11.9998 15C11.4591 15.0001 10.9284 14.8542 10.4639 14.5775C9.99933 14.3009 9.61819 13.9038 9.36076 13.4283C9.10334 12.9528 8.97919 12.4166 9.00146 11.8764C9.02373 11.3362 9.19159 10.812 9.48727 10.3594V10.3594ZM11.9998 18C9.11228 18 6.5904 16.95 4.50915 14.8781C3.64691 14.0329 2.91685 13.0626 2.34352 12C2.78415 11.175 4.19978 8.85936 6.7779 7.36873L8.4654 9.22499C7.81494 10.0615 7.48054 11.1007 7.52112 12.1596C7.5617 13.2185 7.97465 14.2291 8.68723 15.0134C9.39981 15.7976 10.3663 16.3053 11.4164 16.4469C12.4666 16.5885 13.533 16.355 14.4279 15.7875L15.806 17.3062C14.5911 17.771 13.3005 18.0063 11.9998 18V18ZM23.1842 12.3094C23.1467 12.3937 22.1998 14.4937 20.0529 16.4156C19.9152 16.5359 19.7388 16.6025 19.556 16.6031C19.4509 16.6044 19.3467 16.5823 19.2511 16.5385C19.1555 16.4948 19.0707 16.4304 19.0029 16.35C18.937 16.2767 18.8862 16.1912 18.8534 16.0983C18.8205 16.0055 18.8063 15.907 18.8116 15.8086C18.8168 15.7103 18.8413 15.6139 18.8838 15.525C18.9263 15.4361 18.9859 15.3565 19.0592 15.2906C20.107 14.3507 20.9854 13.2376 21.656 12C21.0811 10.9356 20.3513 9.96244 19.4904 9.11248C17.4091 7.04998 14.8873 5.99999 11.9998 5.99999C11.3903 5.99753 10.7818 6.04772 10.181 6.14998C9.98493 6.17994 9.78489 6.13197 9.62371 6.01634C9.46252 5.90071 9.35299 5.72659 9.31852 5.53124C9.30244 5.43397 9.30569 5.33448 9.32809 5.23847C9.35049 5.14246 9.3916 5.0518 9.44907 4.9717C9.50654 4.89159 9.57924 4.8236 9.66301 4.77161C9.74678 4.71963 9.83998 4.68467 9.93727 4.66873C10.6189 4.55517 11.3088 4.49873 11.9998 4.49998C15.2717 4.49998 18.2435 5.74686 20.5966 8.09061C22.3591 9.86249 23.156 11.625 23.1842 11.7C23.2257 11.7962 23.2471 11.8999 23.2471 12.0047C23.2471 12.1095 23.2257 12.2132 23.1842 12.3094V12.3094ZM12.5623 9.05624C12.465 9.03777 12.3723 9.00032 12.2896 8.94604C12.2068 8.89176 12.1355 8.82171 12.0798 8.73987C11.9672 8.57461 11.925 8.37141 11.9623 8.17498C11.9996 7.97856 12.1134 7.80499 12.2786 7.69247C12.4439 7.57995 12.6471 7.53769 12.8435 7.57499C13.7999 7.76202 14.6703 8.25257 15.3257 8.97375C15.981 9.69494 16.3862 10.6083 16.481 11.5781C16.4993 11.7757 16.4385 11.9725 16.312 12.1254C16.1855 12.2782 16.0035 12.3747 15.806 12.3937H15.731C15.5455 12.3945 15.3664 12.3255 15.2292 12.2005C15.0921 12.0755 15.0068 11.9036 14.9904 11.7187C14.9257 11.0729 14.6546 10.4651 14.2172 9.98546C13.7798 9.50585 13.1995 9.18 12.5623 9.05624V9.05624Z"
fill="#C4C4C4"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.1842 11.7C23.1561 11.625 22.3592 9.8625 20.5967 8.09063C18.2436 5.74688 15.2717 4.5 11.9998 4.5C8.72793 4.5 5.75606 5.74688 3.40293 8.09063C1.64043 9.8625 0.843558 11.625 0.815433 11.7C0.773576 11.7945 0.751953 11.8967 0.751953 12C0.751953 12.1033 0.773576 12.2055 0.815433 12.3C0.843558 12.375 1.64043 14.1375 3.40293 15.9094C5.75606 18.2531 8.72793 19.5 11.9998 19.5C15.2717 19.5 18.2436 18.2531 20.5967 15.9094C22.3592 14.1375 23.1561 12.375 23.1842 12.3C23.226 12.2055 23.2477 12.1033 23.2477 12C23.2477 11.8967 23.226 11.7945 23.1842 11.7ZM11.9998 18C9.11231 18 6.59043 16.95 4.50918 14.8781C3.64906 14.031 2.91925 13.0611 2.34356 12C2.91847 10.9356 3.64831 9.96245 4.50918 9.1125C6.59043 7.05 9.11231 6 11.9998 6C14.8873 6 17.4092 7.05 19.4904 9.1125C20.3513 9.96245 21.0811 10.9356 21.6561 12C20.9811 13.2656 18.0373 18 11.9998 18ZM11.9998 7.5C11.1098 7.5 10.2398 7.76392 9.49974 8.25839C8.75972 8.75285 8.18295 9.45566 7.84235 10.2779C7.50176 11.1002 7.41264 12.005 7.58628 12.8779C7.75991 13.7508 8.18849 14.5526 8.81783 15.182C9.44716 15.8113 10.249 16.2399 11.1219 16.4135C11.9948 16.5872 12.8996 16.4981 13.7219 16.1575C14.5442 15.8169 15.247 15.2401 15.7414 14.5001C16.2359 13.76 16.4998 12.89 16.4998 12C16.4998 10.8065 16.0257 9.66193 15.1818 8.81802C14.3379 7.97411 13.1933 7.5 11.9998 7.5ZM11.9998 15C11.4065 15 10.8264 14.8241 10.3331 14.4944C9.83975 14.1648 9.45523 13.6962 9.22817 13.1481C9.00111 12.5999 8.9417 11.9967 9.05745 11.4147C9.17321 10.8328 9.45893 10.2982 9.87849 9.87868C10.298 9.45912 10.8326 9.1734 11.4145 9.05764C11.9965 8.94189 12.5997 9.0013 13.1479 9.22836C13.696 9.45542 14.1646 9.83994 14.4942 10.3333C14.8239 10.8266 14.9998 11.4067 14.9998 12C14.9973 12.7949 14.6805 13.5565 14.1184 14.1186C13.5563 14.6807 12.7947 14.9975 11.9998 15Z" fill="#0099FF"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.1842 11.7C23.1561 11.625 22.3592 9.8625 20.5967 8.09063C18.2436 5.74688 15.2717 4.5 11.9998 4.5C8.72793 4.5 5.75606 5.74688 3.40293 8.09063C1.64043 9.8625 0.843558 11.625 0.815433 11.7C0.773576 11.7945 0.751953 11.8967 0.751953 12C0.751953 12.1033 0.773576 12.2055 0.815433 12.3C0.843558 12.375 1.64043 14.1375 3.40293 15.9094C5.75606 18.2531 8.72793 19.5 11.9998 19.5C15.2717 19.5 18.2436 18.2531 20.5967 15.9094C22.3592 14.1375 23.1561 12.375 23.1842 12.3C23.226 12.2055 23.2477 12.1033 23.2477 12C23.2477 11.8967 23.226 11.7945 23.1842 11.7ZM11.9998 18C9.11231 18 6.59043 16.95 4.50918 14.8781C3.64906 14.031 2.91925 13.0611 2.34356 12C2.91847 10.9356 3.64831 9.96245 4.50918 9.1125C6.59043 7.05 9.11231 6 11.9998 6C14.8873 6 17.4092 7.05 19.4904 9.1125C20.3513 9.96245 21.0811 10.9356 21.6561 12C20.9811 13.2656 18.0373 18 11.9998 18ZM11.9998 7.5C11.1098 7.5 10.2398 7.76392 9.49974 8.25839C8.75972 8.75285 8.18295 9.45566 7.84235 10.2779C7.50176 11.1002 7.41264 12.005 7.58628 12.8779C7.75991 13.7508 8.18849 14.5526 8.81783 15.182C9.44716 15.8113 10.249 16.2399 11.1219 16.4135C11.9948 16.5872 12.8996 16.4981 13.7219 16.1575C14.5442 15.8169 15.247 15.2401 15.7414 14.5001C16.2359 13.76 16.4998 12.89 16.4998 12C16.4998 10.8065 16.0257 9.66193 15.1818 8.81802C14.3379 7.97411 13.1933 7.5 11.9998 7.5ZM11.9998 15C11.4065 15 10.8264 14.8241 10.3331 14.4944C9.83975 14.1648 9.45523 13.6962 9.22817 13.1481C9.00111 12.5999 8.9417 11.9967 9.05745 11.4147C9.17321 10.8328 9.45893 10.2982 9.87849 9.87868C10.298 9.45912 10.8326 9.1734 11.4145 9.05764C11.9965 8.94189 12.5997 9.0013 13.1479 9.22836C13.696 9.45542 14.1646 9.83994 14.4942 10.3333C14.8239 10.8266 14.9998 11.4067 14.9998 12C14.9973 12.7949 14.6805 13.5565 14.1184 14.1186C13.5563 14.6807 12.7947 14.9975 11.9998 15Z"
fill="#0099FF"
/>
</svg>
</template>

View File

@@ -3,14 +3,24 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2381_1348)">
<path d="M5.67116 32C4.61918 31.9997 3.58801 31.7069 2.69286 31.1544C1.79771 30.6018 1.07385 29.8111 0.60217 28.8708C0.130493 27.9305 -0.0704207 26.8776 0.0218796 25.8297C0.11418 24.7818 0.496058 23.7802 1.12484 22.9368C1.75362 22.0934 2.60454 21.4415 3.5825 21.0539C4.56046 20.6663 5.62695 20.5582 6.66277 20.7419C7.69859 20.9255 8.66295 21.3935 9.44809 22.0937C10.2332 22.7938 10.8082 23.6985 11.1088 24.7066H20.2579V21.4652H23.4994V12.1329L19.864 8.49919H10.5334V11.7407H0.80892V2.01621H10.5334V5.2577H19.864L25.1201 0L31.9969 6.87844L26.7408 12.1313V21.4652H29.9823V31.1896H20.2579V27.9481H11.1088C10.7594 29.1189 10.0415 30.1457 9.06182 30.8757C8.08212 31.6057 6.89294 32.0001 5.67116 32ZM5.67116 23.8963C5.35179 23.8964 5.03557 23.9594 4.74056 24.0817C4.44555 24.204 4.17751 24.3832 3.95176 24.6091C3.72601 24.835 3.54697 25.1032 3.42485 25.3983C3.30273 25.6934 3.23993 26.0096 3.24004 26.329C3.24014 26.6484 3.30315 26.9646 3.42547 27.2596C3.54778 27.5546 3.72701 27.8227 3.95291 28.0484C4.17881 28.2742 4.44696 28.4532 4.74206 28.5753C5.03715 28.6974 5.35341 28.7602 5.67278 28.7601C6.31776 28.7599 6.93625 28.5035 7.39217 28.0473C7.8481 27.591 8.10411 26.9724 8.1039 26.3274C8.10368 25.6824 7.84725 25.0639 7.39103 24.608C6.9348 24.1521 6.31614 23.8961 5.67116 23.8963ZM26.7408 24.7066H23.4994V27.9481H26.7408V24.7066ZM25.1201 4.58671L22.8284 6.87844L25.1201 9.17018L27.4118 6.87844L25.1201 4.58671ZM7.2919 5.2577H4.05041V8.49919H7.2919V5.2577Z" fill="#191C21" stroke="white"/>
</g>
<defs>
<clipPath id="clip0_2381_1348">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2381_1348)">
<path
d="M5.67116 32C4.61918 31.9997 3.58801 31.7069 2.69286 31.1544C1.79771 30.6018 1.07385 29.8111 0.60217 28.8708C0.130493 27.9305 -0.0704207 26.8776 0.0218796 25.8297C0.11418 24.7818 0.496058 23.7802 1.12484 22.9368C1.75362 22.0934 2.60454 21.4415 3.5825 21.0539C4.56046 20.6663 5.62695 20.5582 6.66277 20.7419C7.69859 20.9255 8.66295 21.3935 9.44809 22.0937C10.2332 22.7938 10.8082 23.6985 11.1088 24.7066H20.2579V21.4652H23.4994V12.1329L19.864 8.49919H10.5334V11.7407H0.80892V2.01621H10.5334V5.2577H19.864L25.1201 0L31.9969 6.87844L26.7408 12.1313V21.4652H29.9823V31.1896H20.2579V27.9481H11.1088C10.7594 29.1189 10.0415 30.1457 9.06182 30.8757C8.08212 31.6057 6.89294 32.0001 5.67116 32ZM5.67116 23.8963C5.35179 23.8964 5.03557 23.9594 4.74056 24.0817C4.44555 24.204 4.17751 24.3832 3.95176 24.6091C3.72601 24.835 3.54697 25.1032 3.42485 25.3983C3.30273 25.6934 3.23993 26.0096 3.24004 26.329C3.24014 26.6484 3.30315 26.9646 3.42547 27.2596C3.54778 27.5546 3.72701 27.8227 3.95291 28.0484C4.17881 28.2742 4.44696 28.4532 4.74206 28.5753C5.03715 28.6974 5.35341 28.7602 5.67278 28.7601C6.31776 28.7599 6.93625 28.5035 7.39217 28.0473C7.8481 27.591 8.10411 26.9724 8.1039 26.3274C8.10368 25.6824 7.84725 25.0639 7.39103 24.608C6.9348 24.1521 6.31614 23.8961 5.67116 23.8963ZM26.7408 24.7066H23.4994V27.9481H26.7408V24.7066ZM25.1201 4.58671L22.8284 6.87844L25.1201 9.17018L27.4118 6.87844L25.1201 4.58671ZM7.2919 5.2577H4.05041V8.49919H7.2919V5.2577Z"
fill="#191C21"
stroke="white"
/>
</g>
<defs>
<clipPath id="clip0_2381_1348">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
</template>

View File

@@ -3,7 +3,15 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#86909C" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 3.58929C1.5 3.03517 1.72012 2.50375 2.11194 2.11194C2.50375 1.72012 3.03517 1.5 3.58929 1.5H7.76786C8.32197 1.5 8.85339 1.72012 9.24521 2.11194C9.63702 2.50375 9.85714 3.03517 9.85714 3.58929V7.76786C9.85714 8.32197 9.63702 8.85339 9.24521 9.24521C8.85339 9.63702 8.32197 9.85714 7.76786 9.85714H3.58929C3.03517 9.85714 2.50375 9.63702 2.11194 9.24521C1.72012 8.85339 1.5 8.32197 1.5 7.76786V3.58929ZM3.58929 2.89286C3.40458 2.89286 3.22744 2.96623 3.09684 3.09684C2.96623 3.22744 2.89286 3.40458 2.89286 3.58929V7.76786C2.89286 7.95256 2.96623 8.1297 3.09684 8.26031C3.22744 8.39091 3.40458 8.46429 3.58929 8.46429H7.76786C7.95256 8.46429 8.1297 8.39091 8.26031 8.26031C8.39091 8.1297 8.46429 7.95256 8.46429 7.76786V3.58929C8.46429 3.40458 8.39091 3.22744 8.26031 3.09684C8.1297 2.96623 7.95256 2.89286 7.76786 2.89286H3.58929ZM12.6429 3.58929C12.6429 3.03517 12.863 2.50375 13.2548 2.11194C13.6466 1.72012 14.178 1.5 14.7321 1.5H18.9107C19.4648 1.5 19.9962 1.72012 20.3881 2.11194C20.7799 2.50375 21 3.03517 21 3.58929V7.76786C21 8.32197 20.7799 8.85339 20.3881 9.24521C19.9962 9.63702 19.4648 9.85714 18.9107 9.85714H14.7321C14.178 9.85714 13.6466 9.63702 13.2548 9.24521C12.863 8.85339 12.6429 8.32197 12.6429 7.76786V3.58929ZM14.7321 2.89286C14.5474 2.89286 14.3703 2.96623 14.2397 3.09684C14.1091 3.22744 14.0357 3.40458 14.0357 3.58929V7.76786C14.0357 7.95256 14.1091 8.1297 14.2397 8.26031C14.3703 8.39091 14.5474 8.46429 14.7321 8.46429H18.9107C19.0954 8.46429 19.2726 8.39091 19.4032 8.26031C19.5338 8.1297 19.6071 7.95256 19.6071 7.76786V3.58929C19.6071 3.40458 19.5338 3.22744 19.4032 3.09684C19.2726 2.96623 19.0954 2.89286 18.9107 2.89286H14.7321ZM1.5 14.7321C1.5 14.178 1.72012 13.6466 2.11194 13.2548C2.50375 12.863 3.03517 12.6429 3.58929 12.6429H7.76786C8.32197 12.6429 8.85339 12.863 9.24521 13.2548C9.63702 13.6466 9.85714 14.178 9.85714 14.7321V18.9107C9.85714 19.4648 9.63702 19.9962 9.24521 20.3881C8.85339 20.7799 8.32197 21 7.76786 21H3.58929C3.03517 21 2.50375 20.7799 2.11194 20.3881C1.72012 19.9962 1.5 19.4648 1.5 18.9107V14.7321ZM3.58929 14.0357C3.40458 14.0357 3.22744 14.1091 3.09684 14.2397C2.96623 14.3703 2.89286 14.5474 2.89286 14.7321V18.9107C2.89286 19.0954 2.96623 19.2726 3.09684 19.4032C3.22744 19.5338 3.40458 19.6071 3.58929 19.6071H7.76786C7.95256 19.6071 8.1297 19.5338 8.26031 19.4032C8.39091 19.2726 8.46429 19.0954 8.46429 18.9107V14.7321C8.46429 14.5474 8.39091 14.3703 8.26031 14.2397C8.1297 14.1091 7.95256 14.0357 7.76786 14.0357H3.58929ZM12.6429 14.7321C12.6429 14.178 12.863 13.6466 13.2548 13.2548C13.6466 12.863 14.178 12.6429 14.7321 12.6429H18.9107C19.4648 12.6429 19.9962 12.863 20.3881 13.2548C20.7799 13.6466 21 14.178 21 14.7321V18.9107C21 19.4648 20.7799 19.9962 20.3881 20.3881C19.9962 20.7799 19.4648 21 18.9107 21H14.7321C14.178 21 13.6466 20.7799 13.2548 20.3881C12.863 19.9962 12.6429 19.4648 12.6429 18.9107V14.7321ZM14.7321 14.0357C14.5474 14.0357 14.3703 14.1091 14.2397 14.2397C14.1091 14.3703 14.0357 14.5474 14.0357 14.7321V18.9107C14.0357 19.0954 14.1091 19.2726 14.2397 19.4032C14.3703 19.5338 14.5474 19.6071 14.7321 19.6071H18.9107C19.0954 19.6071 19.2726 19.5338 19.4032 19.4032C19.5338 19.2726 19.6071 19.0954 19.6071 18.9107V14.7321C19.6071 14.5474 19.5338 14.3703 19.4032 14.2397C19.2726 14.1091 19.0954 14.0357 18.9107 14.0357H14.7321Z" />
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="#86909C"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.5 3.58929C1.5 3.03517 1.72012 2.50375 2.11194 2.11194C2.50375 1.72012 3.03517 1.5 3.58929 1.5H7.76786C8.32197 1.5 8.85339 1.72012 9.24521 2.11194C9.63702 2.50375 9.85714 3.03517 9.85714 3.58929V7.76786C9.85714 8.32197 9.63702 8.85339 9.24521 9.24521C8.85339 9.63702 8.32197 9.85714 7.76786 9.85714H3.58929C3.03517 9.85714 2.50375 9.63702 2.11194 9.24521C1.72012 8.85339 1.5 8.32197 1.5 7.76786V3.58929ZM3.58929 2.89286C3.40458 2.89286 3.22744 2.96623 3.09684 3.09684C2.96623 3.22744 2.89286 3.40458 2.89286 3.58929V7.76786C2.89286 7.95256 2.96623 8.1297 3.09684 8.26031C3.22744 8.39091 3.40458 8.46429 3.58929 8.46429H7.76786C7.95256 8.46429 8.1297 8.39091 8.26031 8.26031C8.39091 8.1297 8.46429 7.95256 8.46429 7.76786V3.58929C8.46429 3.40458 8.39091 3.22744 8.26031 3.09684C8.1297 2.96623 7.95256 2.89286 7.76786 2.89286H3.58929ZM12.6429 3.58929C12.6429 3.03517 12.863 2.50375 13.2548 2.11194C13.6466 1.72012 14.178 1.5 14.7321 1.5H18.9107C19.4648 1.5 19.9962 1.72012 20.3881 2.11194C20.7799 2.50375 21 3.03517 21 3.58929V7.76786C21 8.32197 20.7799 8.85339 20.3881 9.24521C19.9962 9.63702 19.4648 9.85714 18.9107 9.85714H14.7321C14.178 9.85714 13.6466 9.63702 13.2548 9.24521C12.863 8.85339 12.6429 8.32197 12.6429 7.76786V3.58929ZM14.7321 2.89286C14.5474 2.89286 14.3703 2.96623 14.2397 3.09684C14.1091 3.22744 14.0357 3.40458 14.0357 3.58929V7.76786C14.0357 7.95256 14.1091 8.1297 14.2397 8.26031C14.3703 8.39091 14.5474 8.46429 14.7321 8.46429H18.9107C19.0954 8.46429 19.2726 8.39091 19.4032 8.26031C19.5338 8.1297 19.6071 7.95256 19.6071 7.76786V3.58929C19.6071 3.40458 19.5338 3.22744 19.4032 3.09684C19.2726 2.96623 19.0954 2.89286 18.9107 2.89286H14.7321ZM1.5 14.7321C1.5 14.178 1.72012 13.6466 2.11194 13.2548C2.50375 12.863 3.03517 12.6429 3.58929 12.6429H7.76786C8.32197 12.6429 8.85339 12.863 9.24521 13.2548C9.63702 13.6466 9.85714 14.178 9.85714 14.7321V18.9107C9.85714 19.4648 9.63702 19.9962 9.24521 20.3881C8.85339 20.7799 8.32197 21 7.76786 21H3.58929C3.03517 21 2.50375 20.7799 2.11194 20.3881C1.72012 19.9962 1.5 19.4648 1.5 18.9107V14.7321ZM3.58929 14.0357C3.40458 14.0357 3.22744 14.1091 3.09684 14.2397C2.96623 14.3703 2.89286 14.5474 2.89286 14.7321V18.9107C2.89286 19.0954 2.96623 19.2726 3.09684 19.4032C3.22744 19.5338 3.40458 19.6071 3.58929 19.6071H7.76786C7.95256 19.6071 8.1297 19.5338 8.26031 19.4032C8.39091 19.2726 8.46429 19.0954 8.46429 18.9107V14.7321C8.46429 14.5474 8.39091 14.3703 8.26031 14.2397C8.1297 14.1091 7.95256 14.0357 7.76786 14.0357H3.58929ZM12.6429 14.7321C12.6429 14.178 12.863 13.6466 13.2548 13.2548C13.6466 12.863 14.178 12.6429 14.7321 12.6429H18.9107C19.4648 12.6429 19.9962 12.863 20.3881 13.2548C20.7799 13.6466 21 14.178 21 14.7321V18.9107C21 19.4648 20.7799 19.9962 20.3881 20.3881C19.9962 20.7799 19.4648 21 18.9107 21H14.7321C14.178 21 13.6466 20.7799 13.2548 20.3881C12.863 19.9962 12.6429 19.4648 12.6429 18.9107V14.7321ZM14.7321 14.0357C14.5474 14.0357 14.3703 14.1091 14.2397 14.2397C14.1091 14.3703 14.0357 14.5474 14.0357 14.7321V18.9107C14.0357 19.0954 14.1091 19.2726 14.2397 19.4032C14.3703 19.5338 14.5474 19.6071 14.7321 19.6071H18.9107C19.0954 19.6071 19.2726 19.5338 19.4032 19.4032C19.5338 19.2726 19.6071 19.0954 19.6071 18.9107V14.7321C19.6071 14.5474 19.5338 14.3703 19.4032 14.2397C19.2726 14.1091 19.0954 14.0357 18.9107 14.0357H14.7321Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#86909C" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 18C3.75 17.8011 3.82902 17.6103 3.96967 17.4697C4.11032 17.329 4.30109 17.25 4.5 17.25H19.5C19.6989 17.25 19.8897 17.329 20.0303 17.4697C20.171 17.6103 20.25 17.8011 20.25 18C20.25 18.1989 20.171 18.3897 20.0303 18.5303C19.8897 18.671 19.6989 18.75 19.5 18.75H4.5C4.30109 18.75 4.11032 18.671 3.96967 18.5303C3.82902 18.3897 3.75 18.1989 3.75 18ZM3.75 12C3.75 11.8011 3.82902 11.6103 3.96967 11.4697C4.11032 11.329 4.30109 11.25 4.5 11.25H19.5C19.6989 11.25 19.8897 11.329 20.0303 11.4697C20.171 11.6103 20.25 11.8011 20.25 12C20.25 12.1989 20.171 12.3897 20.0303 12.5303C19.8897 12.671 19.6989 12.75 19.5 12.75H4.5C4.30109 12.75 4.11032 12.671 3.96967 12.5303C3.82902 12.3897 3.75 12.1989 3.75 12ZM3.75 6C3.75 5.80109 3.82902 5.61032 3.96967 5.46967C4.11032 5.32902 4.30109 5.25 4.5 5.25H19.5C19.6989 5.25 19.8897 5.32902 20.0303 5.46967C20.171 5.61032 20.25 5.80109 20.25 6C20.25 6.19891 20.171 6.38968 20.0303 6.53033C19.8897 6.67098 19.6989 6.75 19.5 6.75H4.5C4.30109 6.75 4.11032 6.67098 3.96967 6.53033C3.82902 6.38968 3.75 6.19891 3.75 6Z" />
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="#86909C"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.75 18C3.75 17.8011 3.82902 17.6103 3.96967 17.4697C4.11032 17.329 4.30109 17.25 4.5 17.25H19.5C19.6989 17.25 19.8897 17.329 20.0303 17.4697C20.171 17.6103 20.25 17.8011 20.25 18C20.25 18.1989 20.171 18.3897 20.0303 18.5303C19.8897 18.671 19.6989 18.75 19.5 18.75H4.5C4.30109 18.75 4.11032 18.671 3.96967 18.5303C3.82902 18.3897 3.75 18.1989 3.75 18ZM3.75 12C3.75 11.8011 3.82902 11.6103 3.96967 11.4697C4.11032 11.329 4.30109 11.25 4.5 11.25H19.5C19.6989 11.25 19.8897 11.329 20.0303 11.4697C20.171 11.6103 20.25 11.8011 20.25 12C20.25 12.1989 20.171 12.3897 20.0303 12.5303C19.8897 12.671 19.6989 12.75 19.5 12.75H4.5C4.30109 12.75 4.11032 12.671 3.96967 12.5303C3.82902 12.3897 3.75 12.1989 3.75 12ZM3.75 6C3.75 5.80109 3.82902 5.61032 3.96967 5.46967C4.11032 5.32902 4.30109 5.25 4.5 5.25H19.5C19.6989 5.25 19.8897 5.32902 20.0303 5.46967C20.171 5.61032 20.25 5.80109 20.25 6C20.25 6.19891 20.171 6.38968 20.0303 6.53033C19.8897 6.67098 19.6989 6.75 19.5 6.75H4.5C4.30109 6.75 4.11032 6.67098 3.96967 6.53033C3.82902 6.38968 3.75 6.19891 3.75 6Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 11.75C11.3713 11.7524 10.7643 11.9805 10.2895 12.3926C9.81465 12.8048 9.50353 13.3736 9.41271 13.9958C9.32189 14.6179 9.45738 15.252 9.79456 15.7827C10.1317 16.3134 10.6482 16.7054 11.25 16.8875V18.5C11.25 18.6989 11.329 18.8897 11.4697 19.0303C11.6103 19.171 11.8011 19.25 12 19.25C12.1989 19.25 12.3897 19.171 12.5303 19.0303C12.671 18.8897 12.75 18.6989 12.75 18.5V16.8875C13.3518 16.7054 13.8683 16.3134 14.2054 15.7827C14.5426 15.252 14.6781 14.6179 14.5873 13.9958C14.4965 13.3736 14.1854 12.8048 13.7105 12.3926C13.2357 11.9805 12.6287 11.7524 12 11.75ZM12 15.5C11.7775 15.5 11.56 15.434 11.375 15.3104C11.19 15.1868 11.0458 15.0111 10.9606 14.8055C10.8755 14.6 10.8532 14.3738 10.8966 14.1555C10.94 13.9373 11.0472 13.7368 11.2045 13.5795C11.3618 13.4222 11.5623 13.315 11.7805 13.2716C11.9988 13.2282 12.225 13.2505 12.4305 13.3356C12.6361 13.4208 12.8118 13.565 12.9354 13.75C13.059 13.935 13.125 14.1525 13.125 14.375C13.125 14.6734 13.0065 14.9595 12.7955 15.1705C12.5845 15.3815 12.2984 15.5 12 15.5ZM19.5 8.75H16.125V6.125C16.125 5.03098 15.6904 3.98177 14.9168 3.20818C14.1432 2.4346 13.094 2 12 2C10.906 2 9.85677 2.4346 9.08318 3.20818C8.3096 3.98177 7.875 5.03098 7.875 6.125V8.75H4.5C4.10218 8.75 3.72064 8.90804 3.43934 9.18934C3.15804 9.47064 3 9.85218 3 10.25V20.75C3 21.1478 3.15804 21.5294 3.43934 21.8107C3.72064 22.092 4.10218 22.25 4.5 22.25H19.5C19.8978 22.25 20.2794 22.092 20.5607 21.8107C20.842 21.5294 21 21.1478 21 20.75V10.25C21 9.85218 20.842 9.47064 20.5607 9.18934C20.2794 8.90804 19.8978 8.75 19.5 8.75ZM9.375 6.125C9.375 5.42881 9.65156 4.76113 10.1438 4.26884C10.6361 3.77656 11.3038 3.5 12 3.5C12.6962 3.5 13.3639 3.77656 13.8562 4.26884C14.3484 4.76113 14.625 5.42881 14.625 6.125V8.75H9.375V6.125ZM19.5 20.75H4.5V10.25H19.5V20.75Z" fill="#1A1A1A"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 11.75C11.3713 11.7524 10.7643 11.9805 10.2895 12.3926C9.81465 12.8048 9.50353 13.3736 9.41271 13.9958C9.32189 14.6179 9.45738 15.252 9.79456 15.7827C10.1317 16.3134 10.6482 16.7054 11.25 16.8875V18.5C11.25 18.6989 11.329 18.8897 11.4697 19.0303C11.6103 19.171 11.8011 19.25 12 19.25C12.1989 19.25 12.3897 19.171 12.5303 19.0303C12.671 18.8897 12.75 18.6989 12.75 18.5V16.8875C13.3518 16.7054 13.8683 16.3134 14.2054 15.7827C14.5426 15.252 14.6781 14.6179 14.5873 13.9958C14.4965 13.3736 14.1854 12.8048 13.7105 12.3926C13.2357 11.9805 12.6287 11.7524 12 11.75ZM12 15.5C11.7775 15.5 11.56 15.434 11.375 15.3104C11.19 15.1868 11.0458 15.0111 10.9606 14.8055C10.8755 14.6 10.8532 14.3738 10.8966 14.1555C10.94 13.9373 11.0472 13.7368 11.2045 13.5795C11.3618 13.4222 11.5623 13.315 11.7805 13.2716C11.9988 13.2282 12.225 13.2505 12.4305 13.3356C12.6361 13.4208 12.8118 13.565 12.9354 13.75C13.059 13.935 13.125 14.1525 13.125 14.375C13.125 14.6734 13.0065 14.9595 12.7955 15.1705C12.5845 15.3815 12.2984 15.5 12 15.5ZM19.5 8.75H16.125V6.125C16.125 5.03098 15.6904 3.98177 14.9168 3.20818C14.1432 2.4346 13.094 2 12 2C10.906 2 9.85677 2.4346 9.08318 3.20818C8.3096 3.98177 7.875 5.03098 7.875 6.125V8.75H4.5C4.10218 8.75 3.72064 8.90804 3.43934 9.18934C3.15804 9.47064 3 9.85218 3 10.25V20.75C3 21.1478 3.15804 21.5294 3.43934 21.8107C3.72064 22.092 4.10218 22.25 4.5 22.25H19.5C19.8978 22.25 20.2794 22.092 20.5607 21.8107C20.842 21.5294 21 21.1478 21 20.75V10.25C21 9.85218 20.842 9.47064 20.5607 9.18934C20.2794 8.90804 19.8978 8.75 19.5 8.75ZM9.375 6.125C9.375 5.42881 9.65156 4.76113 10.1438 4.26884C10.6361 3.77656 11.3038 3.5 12 3.5C12.6962 3.5 13.3639 3.77656 13.8562 4.26884C14.3484 4.76113 14.625 5.42881 14.625 6.125V8.75H9.375V6.125ZM19.5 20.75H4.5V10.25H19.5V20.75Z"
fill="#1A1A1A"
/>
</svg>
</template>

View File

@@ -3,8 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="40" height="40" viewBox="0 0 40 40" fill="black" xmlns="http://www.w3.org/2000/svg" id="iconMember">
<circle cx="20" cy="20" r="20" fill="white"/>
<path d="M40 20.0018C40.0001 16.6744 39.1701 13.3995 37.5852 10.4739C36.0004 7.5482 33.7107 5.06419 30.9236 3.24683C28.1365 1.42948 24.9401 0.336201 21.6239 0.0660405C18.3076 -0.20412 14.9764 0.357371 11.9319 1.69965C8.88748 3.04193 6.22597 5.12259 4.1885 7.75315C2.15104 10.3837 0.802001 13.481 0.263595 16.7646C-0.274811 20.0481 0.0144265 23.4141 1.10511 26.5577C2.19579 29.7012 4.05344 32.523 6.50981 34.7673L6.7451 34.9634C10.3981 38.208 15.1142 40 20 40C24.8858 40 29.6019 38.208 33.2549 34.9634L33.4902 34.7673C35.5431 32.8948 37.1826 30.6143 38.3035 28.0717C39.4245 25.5291 40.0023 22.7805 40 20.0018ZM2.35294 20.0018C2.349 17.1207 3.05046 14.2824 4.39604 11.7349C5.74163 9.18731 7.69042 7.00795 10.0722 5.38712C12.454 3.76629 15.1964 2.75327 18.0599 2.43653C20.9234 2.11979 23.8209 2.50896 26.4993 3.57007C29.1778 4.63117 31.5557 6.33193 33.4255 8.52381C35.2952 10.7157 36.6 13.332 37.2257 16.1444C37.8515 18.9567 37.7792 21.8795 37.0153 24.6574C36.2513 27.4354 34.8189 29.9841 32.8431 32.0809C30.9901 29.2141 28.2281 27.0527 25 25.9433C26.6114 24.8699 27.8347 23.3065 28.489 21.4841C29.1433 19.6617 29.1938 17.6772 28.6331 15.8239C28.0725 13.9706 26.9303 12.3469 25.3756 11.1929C23.8209 10.0389 21.9362 9.41584 20 9.41584C18.0638 9.41584 16.1791 10.0389 14.6244 11.1929C13.0697 12.3469 11.9275 13.9706 11.3669 15.8239C10.8062 17.6772 10.8567 19.6617 11.511 21.4841C12.1653 23.3065 13.3886 24.8699 15 25.9433C11.7719 27.0527 9.00992 29.2141 7.15686 32.0809C4.06852 28.8179 2.34915 24.4947 2.35294 20.0018ZM13.3333 18.433C13.3333 17.1144 13.7243 15.8254 14.4569 14.729C15.1894 13.6326 16.2306 12.7781 17.4488 12.2735C18.667 11.7689 20.0074 11.6369 21.3006 11.8941C22.5938 12.1514 23.7817 12.7863 24.714 13.7187C25.6464 14.6511 26.2813 15.8391 26.5386 17.1324C26.7958 18.4256 26.6638 19.7662 26.1592 20.9844C25.6546 22.2027 24.8001 23.2439 23.7038 23.9765C22.6075 24.7091 21.3185 25.1001 20 25.1001C18.2335 25.0949 16.5408 24.3908 15.2917 23.1416C14.0425 21.8925 13.3385 20.1997 13.3333 18.433ZM8.90196 33.728C10.0553 31.8159 11.6831 30.2342 13.6275 29.1362C15.5719 28.0383 17.767 27.4613 20 27.4613C22.233 27.4613 24.428 28.0383 26.3725 29.1362C28.3169 30.2342 29.9447 31.8159 31.098 33.728C27.9556 36.2653 24.0388 37.6492 20 37.6492C15.9612 37.6492 12.0444 36.2653 8.90196 33.728Z"/>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="black"
xmlns="http://www.w3.org/2000/svg"
id="iconMember"
>
<circle cx="20" cy="20" r="20" fill="white" />
<path
d="M40 20.0018C40.0001 16.6744 39.1701 13.3995 37.5852 10.4739C36.0004 7.5482 33.7107 5.06419 30.9236 3.24683C28.1365 1.42948 24.9401 0.336201 21.6239 0.0660405C18.3076 -0.20412 14.9764 0.357371 11.9319 1.69965C8.88748 3.04193 6.22597 5.12259 4.1885 7.75315C2.15104 10.3837 0.802001 13.481 0.263595 16.7646C-0.274811 20.0481 0.0144265 23.4141 1.10511 26.5577C2.19579 29.7012 4.05344 32.523 6.50981 34.7673L6.7451 34.9634C10.3981 38.208 15.1142 40 20 40C24.8858 40 29.6019 38.208 33.2549 34.9634L33.4902 34.7673C35.5431 32.8948 37.1826 30.6143 38.3035 28.0717C39.4245 25.5291 40.0023 22.7805 40 20.0018ZM2.35294 20.0018C2.349 17.1207 3.05046 14.2824 4.39604 11.7349C5.74163 9.18731 7.69042 7.00795 10.0722 5.38712C12.454 3.76629 15.1964 2.75327 18.0599 2.43653C20.9234 2.11979 23.8209 2.50896 26.4993 3.57007C29.1778 4.63117 31.5557 6.33193 33.4255 8.52381C35.2952 10.7157 36.6 13.332 37.2257 16.1444C37.8515 18.9567 37.7792 21.8795 37.0153 24.6574C36.2513 27.4354 34.8189 29.9841 32.8431 32.0809C30.9901 29.2141 28.2281 27.0527 25 25.9433C26.6114 24.8699 27.8347 23.3065 28.489 21.4841C29.1433 19.6617 29.1938 17.6772 28.6331 15.8239C28.0725 13.9706 26.9303 12.3469 25.3756 11.1929C23.8209 10.0389 21.9362 9.41584 20 9.41584C18.0638 9.41584 16.1791 10.0389 14.6244 11.1929C13.0697 12.3469 11.9275 13.9706 11.3669 15.8239C10.8062 17.6772 10.8567 19.6617 11.511 21.4841C12.1653 23.3065 13.3886 24.8699 15 25.9433C11.7719 27.0527 9.00992 29.2141 7.15686 32.0809C4.06852 28.8179 2.34915 24.4947 2.35294 20.0018ZM13.3333 18.433C13.3333 17.1144 13.7243 15.8254 14.4569 14.729C15.1894 13.6326 16.2306 12.7781 17.4488 12.2735C18.667 11.7689 20.0074 11.6369 21.3006 11.8941C22.5938 12.1514 23.7817 12.7863 24.714 13.7187C25.6464 14.6511 26.2813 15.8391 26.5386 17.1324C26.7958 18.4256 26.6638 19.7662 26.1592 20.9844C25.6546 22.2027 24.8001 23.2439 23.7038 23.9765C22.6075 24.7091 21.3185 25.1001 20 25.1001C18.2335 25.0949 16.5408 24.3908 15.2917 23.1416C14.0425 21.8925 13.3385 20.1997 13.3333 18.433ZM8.90196 33.728C10.0553 31.8159 11.6831 30.2342 13.6275 29.1362C15.5719 28.0383 17.767 27.4613 20 27.4613C22.233 27.4613 24.428 28.0383 26.3725 29.1362C28.3169 30.2342 29.9447 31.8159 31.098 33.728C27.9556 36.2653 24.0388 37.6492 20 37.6492C15.9612 37.6492 12.0444 36.2653 8.90196 33.728Z"
/>
</svg>
</template>

View File

@@ -3,24 +3,72 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="3 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_4086_5522)">
<path d="M10.0359 5.48491V10.4086H9.0885L7.18616 7.30519L6.70871 6.43235H6.70125L6.73109 7.23058V10.4086H6V5.48491H6.93998L8.83486 8.58087L9.31977 9.46863H9.32723L9.29739 8.66293V5.48491H10.0359Z" fill="white"/>
<path d="M13.7426 5L11.5195 10.9308H10.7735L12.9966 5H13.7426Z" fill="white"/>
<path d="M18 10.4086H17.157L16.687 9.118H14.6206L14.1506 10.4086H13.3299L15.1875 5.48491H16.1424L18 10.4086ZM14.8444 8.46897H16.4632L15.6575 6.17124L14.8444 8.46897Z" fill="white"/>
<rect x="4" y="0.2" width="16" height="16" rx="8" stroke="white" shape-rendering="geometricPrecision"/>
</g>
<defs>
<filter id="filter0_d_4086_5522" x="0" y="0" width="24" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4086_5522"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4086_5522" result="shape"/>
</filter>
</defs>
</svg>
<svg
width="16"
height="16"
viewBox="3 0 20 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g filter="url(#filter0_d_4086_5522)">
<path
d="M10.0359 5.48491V10.4086H9.0885L7.18616 7.30519L6.70871 6.43235H6.70125L6.73109 7.23058V10.4086H6V5.48491H6.93998L8.83486 8.58087L9.31977 9.46863H9.32723L9.29739 8.66293V5.48491H10.0359Z"
fill="white"
/>
<path
d="M13.7426 5L11.5195 10.9308H10.7735L12.9966 5H13.7426Z"
fill="white"
/>
<path
d="M18 10.4086H17.157L16.687 9.118H14.6206L14.1506 10.4086H13.3299L15.1875 5.48491H16.1424L18 10.4086ZM14.8444 8.46897H16.4632L15.6575 6.17124L14.8444 8.46897Z"
fill="white"
/>
<rect
x="4"
y="0.2"
width="16"
height="16"
rx="8"
stroke="white"
shape-rendering="geometricPrecision"
/>
</g>
<defs>
<filter
id="filter0_d_4086_5522"
x="0"
y="0"
width="24"
height="24"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_4086_5522"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4086_5522"
result="shape"
/>
</filter>
</defs>
</svg>
</template>

View File

@@ -3,8 +3,17 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 16H22V18H10V16ZM10 10H22V12H10V10Z" fill="#191C21"/>
<path d="M16 30L9.82401 26.707C8.06334 25.7703 6.59097 24.3719 5.56493 22.6617C4.53888 20.9516 3.99789 18.9943 4.00001 17V4C4.00054 3.46973 4.21142 2.96133 4.58638 2.58637C4.96134 2.21141 5.46974 2.00053 6.00001 2H26C26.5303 2.00053 27.0387 2.21141 27.4136 2.58637C27.7886 2.96133 27.9995 3.46973 28 4V17C28.0021 18.9943 27.4611 20.9516 26.4351 22.6617C25.409 24.3719 23.9367 25.7703 22.176 26.707L16 30ZM6.00001 4V17C5.99835 18.6318 6.44111 20.2333 7.28077 21.6325C8.12043 23.0317 9.32528 24.1758 10.766 24.942L16 27.733L21.234 24.943C22.6749 24.1767 23.8798 23.0324 24.7195 21.633C25.5592 20.2336 26.0018 18.632 26 17V4H6.00001Z" fill="#191C21"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 16H22V18H10V16ZM10 10H22V12H10V10Z" fill="#191C21" />
<path
d="M16 30L9.82401 26.707C8.06334 25.7703 6.59097 24.3719 5.56493 22.6617C4.53888 20.9516 3.99789 18.9943 4.00001 17V4C4.00054 3.46973 4.21142 2.96133 4.58638 2.58637C4.96134 2.21141 5.46974 2.00053 6.00001 2H26C26.5303 2.00053 27.0387 2.21141 27.4136 2.58637C27.7886 2.96133 27.9995 3.46973 28 4V17C28.0021 18.9943 27.4611 20.9516 26.4351 22.6617C25.409 24.3719 23.9367 25.7703 22.176 26.707L16 30ZM6.00001 4V17C5.99835 18.6318 6.44111 20.2333 7.28077 21.6325C8.12043 23.0317 9.32528 24.1758 10.766 24.942L16 27.733L21.234 24.943C22.6749 24.1767 23.8798 23.0324 24.7195 21.633C25.5592 20.2336 26.0018 18.632 26 17V4H6.00001Z"
fill="#191C21"
/>
</svg>
</template>

View File

@@ -3,8 +3,26 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 19C15.1944 19 19 15.1944 19 10.5C19 5.80558 15.1944 2 10.5 2C5.80558 2 2 5.80558 2 10.5C2 15.1944 5.80558 19 10.5 19Z" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 21L17 17" stroke="#4E5969" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 19C15.1944 19 19 15.1944 19 10.5C19 5.80558 15.1944 2 10.5 2C5.80558 2 2 5.80558 2 10.5C2 15.1944 5.80558 19 10.5 19Z"
stroke="#4E5969"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21 21L17 17"
stroke="#4E5969"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>

View File

@@ -3,7 +3,16 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.5 6H19.425C19.05 4.275 17.55 3 15.75 3C13.95 3 12.45 4.275 12.075 6H1.5V7.5H12.075C12.45 9.225 13.95 10.5 15.75 10.5C17.55 10.5 19.05 9.225 19.425 7.5H22.5V6ZM15.75 9C14.475 9 13.5 8.025 13.5 6.75C13.5 5.475 14.475 4.5 15.75 4.5C17.025 4.5 18 5.475 18 6.75C18 8.025 17.025 9 15.75 9ZM1.5 18H4.575C4.95 19.725 6.45 21 8.25 21C10.05 21 11.55 19.725 11.925 18H22.5V16.5H11.925C11.55 14.775 10.05 13.5 8.25 13.5C6.45 13.5 4.95 14.775 4.575 16.5H1.5V18ZM8.25 15C9.525 15 10.5 15.975 10.5 17.25C10.5 18.525 9.525 19.5 8.25 19.5C6.975 19.5 6 18.525 6 17.25C6 15.975 6.975 15 8.25 15Z" fill="#64748B"/>
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.5 6H19.425C19.05 4.275 17.55 3 15.75 3C13.95 3 12.45 4.275 12.075 6H1.5V7.5H12.075C12.45 9.225 13.95 10.5 15.75 10.5C17.55 10.5 19.05 9.225 19.425 7.5H22.5V6ZM15.75 9C14.475 9 13.5 8.025 13.5 6.75C13.5 5.475 14.475 4.5 15.75 4.5C17.025 4.5 18 5.475 18 6.75C18 8.025 17.025 9 15.75 9ZM1.5 18H4.575C4.95 19.725 6.45 21 8.25 21C10.05 21 11.55 19.725 11.925 18H22.5V16.5H11.925C11.55 14.775 10.05 13.5 8.25 13.5C6.45 13.5 4.95 14.775 4.575 16.5H1.5V18ZM8.25 15C9.525 15 10.5 15.975 10.5 17.25C10.5 18.525 9.525 19.5 8.25 19.5C6.975 19.5 6 18.525 6 17.25C6 15.975 6.975 15 8.25 15Z"
fill="#64748B"
/>
</svg>
</template>

View File

@@ -3,14 +3,23 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
>
<g clip-path="url(#clip0_2395_2657)">
<circle cx="32" cy="32" r="30" fill="#0099FF"/>
<path d="M33.7678 22.2322C32.7915 21.2559 31.2085 21.2559 30.2322 22.2322L14.3223 38.1421C13.346 39.1185 13.346 40.7014 14.3223 41.6777C15.2986 42.654 16.8816 42.654 17.8579 41.6777L32 27.5355L46.1421 41.6777C47.1185 42.654 48.7014 42.654 49.6777 41.6777C50.654 40.7014 50.654 39.1184 49.6777 38.1421L33.7678 22.2322ZM29.5 64C29.5 65.3807 30.6193 66.5 32 66.5C33.3807 66.5 34.5 65.3807 34.5 64H29.5ZM29.5 24L29.5 64H34.5L34.5 24L29.5 24Z" fill="white"/>
<circle cx="32" cy="32" r="30" fill="#0099FF" />
<path
d="M33.7678 22.2322C32.7915 21.2559 31.2085 21.2559 30.2322 22.2322L14.3223 38.1421C13.346 39.1185 13.346 40.7014 14.3223 41.6777C15.2986 42.654 16.8816 42.654 17.8579 41.6777L32 27.5355L46.1421 41.6777C47.1185 42.654 48.7014 42.654 49.6777 41.6777C50.654 40.7014 50.654 39.1184 49.6777 38.1421L33.7678 22.2322ZM29.5 64C29.5 65.3807 30.6193 66.5 32 66.5C33.3807 66.5 34.5 65.3807 34.5 64H29.5ZM29.5 24L29.5 64H34.5L34.5 24L29.5 24Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_2395_2657">
<rect width="64" height="64" fill="white"/>
<rect width="64" height="64" fill="white" />
</clipPath>
</defs>
</svg>

View File

@@ -3,7 +3,15 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9999 16C7.71657 16 7.47924 15.904 7.2879 15.712C7.0959 15.5207 6.9999 15.2833 6.9999 15V3.82499L2.1249 8.69999C1.9249 8.89999 1.68724 8.99999 1.4119 8.99999C1.13724 8.99999 0.899902 8.89999 0.699902 8.69999C0.499902 8.49999 0.399902 8.26665 0.399902 7.99999C0.399902 7.73332 0.499902 7.49999 0.699902 7.29999L7.2999 0.699987C7.3999 0.599987 7.50824 0.528988 7.6249 0.486988C7.74157 0.445654 7.86657 0.424988 7.9999 0.424988C8.13324 0.424988 8.26257 0.445654 8.3879 0.486988C8.51257 0.528988 8.61657 0.599987 8.6999 0.699987L15.2999 7.29999C15.4999 7.49999 15.5999 7.73332 15.5999 7.99999C15.5999 8.26665 15.4999 8.49999 15.2999 8.69999C15.0999 8.89999 14.8622 8.99999 14.5869 8.99999C14.3122 8.99999 14.0749 8.89999 13.8749 8.69999L8.9999 3.82499V15C8.9999 15.2833 8.90424 15.5207 8.7129 15.712C8.5209 15.904 8.28324 16 7.9999 16Z"/>
</svg>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.9999 16C7.71657 16 7.47924 15.904 7.2879 15.712C7.0959 15.5207 6.9999 15.2833 6.9999 15V3.82499L2.1249 8.69999C1.9249 8.89999 1.68724 8.99999 1.4119 8.99999C1.13724 8.99999 0.899902 8.89999 0.699902 8.69999C0.499902 8.49999 0.399902 8.26665 0.399902 7.99999C0.399902 7.73332 0.499902 7.49999 0.699902 7.29999L7.2999 0.699987C7.3999 0.599987 7.50824 0.528988 7.6249 0.486988C7.74157 0.445654 7.86657 0.424988 7.9999 0.424988C8.13324 0.424988 8.26257 0.445654 8.3879 0.486988C8.51257 0.528988 8.61657 0.599987 8.6999 0.699987L15.2999 7.29999C15.4999 7.49999 15.5999 7.73332 15.5999 7.99999C15.5999 8.26665 15.4999 8.49999 15.2999 8.69999C15.0999 8.89999 14.8622 8.99999 14.5869 8.99999C14.3122 8.99999 14.0749 8.89999 13.8749 8.69999L8.9999 3.82499V15C8.9999 15.2833 8.90424 15.5207 8.7129 15.712C8.5209 15.904 8.28324 16 7.9999 16Z"
/>
</svg>
</template>

View File

@@ -3,7 +3,18 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.78488 2.03878C9.62343 1.72766 9.37026 1.46508 9.05455 1.28129C8.73885 1.09751 8.37345 1 8.00047 1C7.62748 1 7.26209 1.09751 6.94638 1.28129C6.63068 1.46508 6.37751 1.72766 6.21605 2.03878L0.521086 12.4048C-0.160262 13.6423 0.70072 15.2857 2.3048 15.2857H13.6954C15.3002 15.2857 16.1598 13.643 15.4799 12.4048L9.78488 2.03878ZM8.00047 5.54628C8.18657 5.54628 8.36505 5.61468 8.49664 5.73645C8.62824 5.85822 8.70216 6.02337 8.70216 6.19557V9.44205C8.70216 9.61425 8.62824 9.77941 8.49664 9.90117C8.36505 10.0229 8.18657 10.0913 8.00047 10.0913C7.81437 10.0913 7.63589 10.0229 7.50429 9.90117C7.3727 9.77941 7.29877 9.61425 7.29877 9.44205V6.19557C7.29877 6.02337 7.3727 5.85822 7.50429 5.73645C7.63589 5.61468 7.81437 5.54628 8.00047 5.54628ZM8.00047 11.0653C8.18657 11.0653 8.36505 11.1337 8.49664 11.2555C8.62824 11.3772 8.70216 11.5424 8.70216 11.7146V12.0392C8.70216 12.2114 8.62824 12.3766 8.49664 12.4984C8.36505 12.6201 8.18657 12.6885 8.00047 12.6885C7.81437 12.6885 7.63589 12.6201 7.50429 12.4984C7.3727 12.3766 7.29877 12.2114 7.29877 12.0392V11.7146C7.29877 11.5424 7.3727 11.3772 7.50429 11.2555C7.63589 11.1337 7.81437 11.0653 8.00047 11.0653Z" fill="#FF3366"/>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.78488 2.03878C9.62343 1.72766 9.37026 1.46508 9.05455 1.28129C8.73885 1.09751 8.37345 1 8.00047 1C7.62748 1 7.26209 1.09751 6.94638 1.28129C6.63068 1.46508 6.37751 1.72766 6.21605 2.03878L0.521086 12.4048C-0.160262 13.6423 0.70072 15.2857 2.3048 15.2857H13.6954C15.3002 15.2857 16.1598 13.643 15.4799 12.4048L9.78488 2.03878ZM8.00047 5.54628C8.18657 5.54628 8.36505 5.61468 8.49664 5.73645C8.62824 5.85822 8.70216 6.02337 8.70216 6.19557V9.44205C8.70216 9.61425 8.62824 9.77941 8.49664 9.90117C8.36505 10.0229 8.18657 10.0913 8.00047 10.0913C7.81437 10.0913 7.63589 10.0229 7.50429 9.90117C7.3727 9.77941 7.29877 9.61425 7.29877 9.44205V6.19557C7.29877 6.02337 7.3727 5.85822 7.50429 5.73645C7.63589 5.61468 7.81437 5.54628 8.00047 5.54628ZM8.00047 11.0653C8.18657 11.0653 8.36505 11.1337 8.49664 11.2555C8.62824 11.3772 8.70216 11.5424 8.70216 11.7146V12.0392C8.70216 12.2114 8.62824 12.3766 8.49664 12.4984C8.36505 12.6201 8.18657 12.6885 8.00047 12.6885C7.81437 12.6885 7.63589 12.6201 7.50429 12.4984C7.3727 12.3766 7.29877 12.2114 7.29877 12.0392V11.7146C7.29877 11.5424 7.3727 11.3772 7.50429 11.2555C7.63589 11.1337 7.81437 11.0653 8.00047 11.0653Z"
fill="#FF3366"
/>
</svg>
</template>

View File

@@ -3,8 +3,26 @@
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31 -->
<template>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 9.33333C22.6274 9.33333 28 8.13943 28 6.66667C28 5.19391 22.6274 4 16 4C9.37258 4 4 5.19391 4 6.66667C4 8.13943 9.37258 9.33333 16 9.33333Z" stroke="#191C21" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 6.66666C4 9.63999 9.16133 15.5653 11.808 18.4067C12.7789 19.4358 13.3239 20.7945 13.3333 22.2093V25.3333C13.3333 26.0406 13.6143 26.7188 14.1144 27.2189C14.6145 27.719 15.2928 28 16 28C16.7072 28 17.3855 27.719 17.8856 27.2189C18.3857 26.7188 18.6667 26.0406 18.6667 25.3333V22.2093C18.6667 20.7947 19.228 19.4427 20.192 18.4067C22.84 15.5653 28 9.64132 28 6.66666" stroke="#191C21" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 9.33333C22.6274 9.33333 28 8.13943 28 6.66667C28 5.19391 22.6274 4 16 4C9.37258 4 4 5.19391 4 6.66667C4 8.13943 9.37258 9.33333 16 9.33333Z"
stroke="#191C21"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4 6.66666C4 9.63999 9.16133 15.5653 11.808 18.4067C12.7789 19.4358 13.3239 20.7945 13.3333 22.2093V25.3333C13.3333 26.0406 13.6143 26.7188 14.1144 27.2189C14.6145 27.719 15.2928 28 16 28C16.7072 28 17.3855 27.719 17.8856 27.2189C18.3857 26.7188 18.6667 26.0406 18.6667 25.3333V22.2093C18.6667 20.7947 19.228 19.4427 20.192 18.4067C22.84 15.5653 28 9.64132 28 6.66666"
stroke="#191C21"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>

View File

@@ -14,19 +14,19 @@ export const ONCE_RENDER_NUM_OF_DATA = 9;
export const PWD_VALID_LENGTH = 6;
/** @constant {string} Default grid line color (Tailwind slate-500). */
export const GRID_COLOR = '#64748b';
export const GRID_COLOR = "#64748b";
/** @constant {string} Modal type for creating a new account. */
export const MODAL_CREATE_NEW = 'MODAL_CREATE_NEW';
export const MODAL_CREATE_NEW = "MODAL_CREATE_NEW";
/** @constant {string} Modal type for editing an account. */
export const MODAL_ACCT_EDIT = 'MODAL_ACCT_EDIT';
export const MODAL_ACCT_EDIT = "MODAL_ACCT_EDIT";
/** @constant {string} Modal type for viewing account info. */
export const MODAL_ACCT_INFO = 'MODAL_ACCT_INFO';
export const MODAL_ACCT_INFO = "MODAL_ACCT_INFO";
/** @constant {string} Modal type for deleting an account. */
export const MODAL_DELETE = 'MODAL_DELETE';
export const MODAL_DELETE = "MODAL_DELETE";
/** @constant {string} LocalStorage key for saved Cytoscape node positions. */
export const SAVE_KEY_NAME = 'CYTOSCAPE_NODE_POSITION';
export const SAVE_KEY_NAME = "CYTOSCAPE_NODE_POSITION";
/** @constant {number} Duration (minutes) to highlight newly created accounts. */
export const JUST_CREATE_ACCOUNT_HOT_DURATION_MINS = 2;
@@ -36,171 +36,171 @@ export const JUST_CREATE_ACCOUNT_HOT_DURATION_MINS = 2;
* process insights (self-loops, short-loops, traces).
*/
export const INSIGHTS_FIELDS_AND_LABELS = [
['self_loops', 'Self-Loop'],
['short_loops', 'Short-Loop'],
['shortest_traces', 'Shortest Trace'],
['longest_traces', 'Longest Trace'],
['most_freq_traces', 'Most Frequent Trace'],
];
["self_loops", "Self-Loop"],
["short_loops", "Short-Loop"],
["shortest_traces", "Shortest Trace"],
["longest_traces", "Longest Trace"],
["most_freq_traces", "Most Frequent Trace"],
];
/** @constant {Object} Default Chart.js layout padding options. */
export const knownLayoutChartOption = {
padding: {
top: 16,
left: 8,
right: 8,
}
padding: {
top: 16,
left: 8,
right: 8,
},
};
/** @constant {Object} Default Chart.js scale options for line charts. */
export const knownScaleLineChartOptions = {
x: {
type: 'time',
export const knownScaleLineChartOptions = {
x: {
type: "time",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
time: {
displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11
minute: 'M/d h:mm', // ex: 1/1 1:11
hour: 'M/d h:mm', // ex: 1/1 1:11
day: 'M/d h', // ex: 1/1 1
month: 'y/M/d', // ex: 1911/1/1
},
displayFormats: {
second: "h:mm:ss", // ex: 1:11:11
minute: "M/d h:mm", // ex: 1/1 1:11
hour: "M/d h:mm", // ex: 1/1 1:11
day: "M/d h", // ex: 1/1 1
month: "y/M/d", // ex: 1911/1/1
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
source: 'labels', // Proportionally display the number of labels
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
source: "labels", // Proportionally display the number of labels
},
border: {
color: '#64748b',
color: "#64748b",
},
grid: {
tickLength: 0, // Whether grid lines extend beyond the axis
}
},
y: {
tickLength: 0, // Whether grid lines extend beyond the axis
},
},
y: {
beginAtZero: true, // Scale includes 0
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
lineHeight: 2,
},
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
color: '#64748b',
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
},
};
/** @constant {Object} Default Chart.js scale options for horizontal charts. */
export const knownScaleHorizontalChartOptions = {
x: {
x: {
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
},
grid: {
color: '#64748b',
tickLength: 0, // Whether grid lines extend beyond the axis
color: "#64748b",
tickLength: 0, // Whether grid lines extend beyond the axis
},
border: {
display:false,
display: false,
},
},
y: {
},
y: {
beginAtZero: true, // Scale includes 0
type: 'category',
type: "category",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
lineHeight: 2,
},
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
display:false,
color: '#64748b',
display: false,
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
},
};
/** @constant {Object} Default Chart.js scale options for bar charts. */
export const knownScaleBarChartOptions = {
x: {
x: {
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: '#64748b',
display: true,
maxRotation: 0, // Do not rotate labels (range 0~50)
color: "#64748b",
},
grid: {
color: '#64748b',
tickLength: 0, // Whether grid lines extend beyond the axis
color: "#64748b",
tickLength: 0, // Whether grid lines extend beyond the axis
},
border: {
display:false,
display: false,
},
},
y: {
},
y: {
beginAtZero: true, // Scale includes 0
type: 'category',
type: "category",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
},
lineHeight: 2,
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
display:false,
color: '#64748b',
display: false,
color: "#64748b",
},
border: {
display: false, // Hide the extra line on the left side
display: false, // Hide the extra line on the left side
},
},
};
},
};

View File

@@ -9,24 +9,21 @@
* with browser language detection.
*/
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enLocale from './en.json';
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import enLocale from "./en.json";
i18next
.use(LanguageDetector)
.init({
resources: {
en: {
translation: enLocale
},
i18next.use(LanguageDetector).init({
resources: {
en: {
translation: enLocale,
},
fallbackLng: 'en',
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage']
},
});
},
fallbackLng: "en",
detection: {
order: ["localStorage", "navigator"],
caches: ["localStorage"],
},
});
export default i18next;
export default i18next;

View File

@@ -14,45 +14,45 @@ import { createApp, markRaw } from "vue";
import App from "./App.vue";
import router from "./router";
import pinia from '@/stores/main';
import ToastPlugin from 'vue-toast-notification';
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import popper from 'cytoscape-popper';
import draggable from 'vuedraggable';
import VueSweetalert2 from 'vue-sweetalert2';
import pinia from "@/stores/main";
import ToastPlugin from "vue-toast-notification";
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import popper from "cytoscape-popper";
import draggable from "vuedraggable";
import VueSweetalert2 from "vue-sweetalert2";
// import CSS
import "./assets/main.css";
import 'vue-toast-notification/dist/theme-sugar.css';
import 'sweetalert2/dist/sweetalert2.min.css';
import "vue-toast-notification/dist/theme-sugar.css";
import "sweetalert2/dist/sweetalert2.min.css";
// import PrimeVue
import PrimeVue from 'primevue/config';
import Aura from '@primevue/themes/aura';
import 'primeicons/primeicons.css'; //icons
import Sidebar from 'primevue/sidebar';
import Dropdown from 'primevue/dropdown';
import Tag from 'primevue/tag';
import ProgressBar from 'primevue/progressbar';
import TabView from 'primevue/tabview';
import TabPanel from 'primevue/tabpanel';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import ColumnGroup from 'primevue/columngroup'; // optional
import Row from 'primevue/row'; // optional
import RadioButton from 'primevue/radiobutton';
import Timeline from 'primevue/timeline';
import InputSwitch from 'primevue/inputswitch';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import Chart from 'primevue/chart';
import Slider from 'primevue/slider';
import Calendar from 'primevue/calendar';
import Tooltip from 'primevue/tooltip';
import Checkbox from 'primevue/checkbox';
import Dialog from 'primevue/dialog';
import ContextMenu from 'primevue/contextmenu';
import PrimeVue from "primevue/config";
import Aura from "@primevue/themes/aura";
import "primeicons/primeicons.css"; //icons
import Sidebar from "primevue/sidebar";
import Dropdown from "primevue/dropdown";
import Tag from "primevue/tag";
import ProgressBar from "primevue/progressbar";
import TabView from "primevue/tabview";
import TabPanel from "primevue/tabpanel";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import ColumnGroup from "primevue/columngroup"; // optional
import Row from "primevue/row"; // optional
import RadioButton from "primevue/radiobutton";
import Timeline from "primevue/timeline";
import InputSwitch from "primevue/inputswitch";
import InputNumber from "primevue/inputnumber";
import InputText from "primevue/inputtext";
import Chart from "primevue/chart";
import Slider from "primevue/slider";
import Calendar from "primevue/calendar";
import Tooltip from "primevue/tooltip";
import Checkbox from "primevue/checkbox";
import Dialog from "primevue/dialog";
import ContextMenu from "primevue/contextmenu";
const app = createApp(App);
@@ -62,39 +62,40 @@ pinia.use(({ store }) => {
});
// Cytoscape.js's style
cytoscape.use( dagre );
cytoscape.use( popper((ref) => ref) );
cytoscape.use(dagre);
cytoscape.use(popper((ref) => ref));
app.use(pinia);
app.use(router);
app.use(VueSweetalert2);
app.use(ToastPlugin, { // use `this.$toast` in Vue.js
position: 'bottom',
app.use(ToastPlugin, {
// use `this.$toast` in Vue.js
position: "bottom",
duration: 5000,
});
app.use(PrimeVue, { theme: { preset: Aura } });
app.component('Sidebar', Sidebar);
app.component('Dropdown', Dropdown);
app.component('Tag', Tag);
app.component('ProgressBar', ProgressBar);
app.component('TabView', TabView);
app.component('TabPanel', TabPanel);
app.component('DataTable', DataTable);
app.component('Column', Column);
app.component('ColumnGroup', ColumnGroup);
app.component('Row', Row);
app.component('RadioButton', RadioButton);
app.component('Timeline', Timeline);
app.component('InputSwitch', InputSwitch);
app.component('InputNumber', InputNumber);
app.component('InputText', InputText);
app.component('Chart', Chart);
app.component('Slider', Slider);
app.component('Calendar', Calendar);
app.component('Checkbox', Checkbox);
app.component('Dialog', Dialog);
app.component('ContextMenu', ContextMenu);
app.component('Draggable', draggable); // Drag and drop
app.directive('tooltip', Tooltip);
app.component("Sidebar", Sidebar);
app.component("Dropdown", Dropdown);
app.component("Tag", Tag);
app.component("ProgressBar", ProgressBar);
app.component("TabView", TabView);
app.component("TabPanel", TabPanel);
app.component("DataTable", DataTable);
app.component("Column", Column);
app.component("ColumnGroup", ColumnGroup);
app.component("Row", Row);
app.component("RadioButton", RadioButton);
app.component("Timeline", Timeline);
app.component("InputSwitch", InputSwitch);
app.component("InputNumber", InputNumber);
app.component("InputText", InputText);
app.component("Chart", Chart);
app.component("Slider", Slider);
app.component("Calendar", Calendar);
app.component("Checkbox", Checkbox);
app.component("Dialog", Dialog);
app.component("ContextMenu", ContextMenu);
app.component("Draggable", draggable); // Drag and drop
app.directive("tooltip", Tooltip);
app.mount("#app");

View File

@@ -19,23 +19,23 @@ export default function abbreviateNumber(totalSeconds) {
let minutes = 0;
let hours = 0;
let days = 0;
let result = '';
let symbols = ['d', 'h', 'm', 's'];
let result = "";
let symbols = ["d", "h", "m", "s"];
totalSeconds = parseInt(totalSeconds);
if(!isNaN(totalSeconds)) {
if (!isNaN(totalSeconds)) {
seconds = totalSeconds % 60;
minutes = (Math.floor(totalSeconds - seconds) / 60) % 60;
hours = (Math.floor(totalSeconds / 3600)) % 24;
hours = Math.floor(totalSeconds / 3600) % 24;
days = Math.floor(totalSeconds / (3600 * 24));
};
}
const units = [days, hours, minutes, seconds];
for(let i = 0; i < units.length; i++) {
if(units[i] > 0) result += units[i] + symbols[i] + " ";
for (let i = 0; i < units.length; i++) {
if (units[i] > 0) result += units[i] + symbols[i] + " ";
}
result = result.trim();
if(totalSeconds === 0) result = '0';
if (totalSeconds === 0) result = "0";
return result;
};
}

View File

@@ -6,24 +6,27 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module alertModal SweetAlert2 modal dialogs for user interactions. */
import Swal from 'sweetalert2';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { useFilesStore } from '@/stores/files';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useModalStore } from '@/stores/modal';
import { escapeHtml } from '@/utils/escapeHtml.js';
import Swal from "sweetalert2";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { useFilesStore } from "@/stores/files";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useModalStore } from "@/stores/modal";
import { escapeHtml } from "@/utils/escapeHtml.js";
const customClass = {
container: '!z-[99999]',
popup: '!w-[564px]',
title: '!text-xl !font-semibold !mb-2',
htmlContainer: '!text-sm !font-normal !h-full !mb-4 !leading-5',
inputLabel: '!text-sm !font-normal',
input: '!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200',
validationMessage: '!bg-neutral-10 !text-danger',
confirmButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]',
cancelButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ',
container: "!z-[99999]",
popup: "!w-[564px]",
title: "!text-xl !font-semibold !mb-2",
htmlContainer: "!text-sm !font-normal !h-full !mb-4 !leading-5",
inputLabel: "!text-sm !font-normal",
input:
"!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200",
validationMessage: "!bg-neutral-10 !text-danger",
confirmButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]",
cancelButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ",
};
/**
* Shows a modal dialog to save a new filter with a user-provided name.
@@ -33,47 +36,51 @@ const customClass = {
* @returns {Promise<boolean>} True if the filter was saved, false otherwise.
*/
export async function saveFilter(addFilterId, next = null) {
let fileName = '';
let fileName = "";
const pageAdminStore = usePageAdminStore();
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW FILTER',
input: 'text',
inputPlaceholder: 'Enter Filter Name.',
title: "SAVE NEW FILTER",
input: "text",
inputPlaceholder: "Enter Filter Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addFilterId(fileName);
// Show save complete notification
if (value) { // Example of value: yes
if (value) {
// Example of value: yes
savedSuccessfully(value);
}
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
pageAdminStore.keepPreviousPage();
// Not every time we have nontrivial next value
if (next !== null) {
next(false);
}
}
return false;
}
}
@@ -84,17 +91,17 @@ export async function saveFilter(addFilterId, next = null) {
* @returns {Promise<void>}
*/
export async function savedSuccessfully(value) {
value = value || '';
value = value || "";
await Swal.fire({
title: 'SAVE COMPLETE',
title: "SAVE COMPLETE",
html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Prompts the user to save unsaved filter changes before leaving the
* Map page. Handles confirm (save), cancel (discard), and backdrop
@@ -112,23 +119,23 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
const pageAdminStore = usePageAdminStore();
const result = await Swal.fire({
title: 'SAVE YOUR FILTER?',
html: 'If you want to continue using this filter in any other page, please select [Yes].',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "SAVE YOUR FILTER?",
html: "If you want to continue using this filter in any other page, please select [Yes].",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if(allMapDataStore.createFilterId) {
if (result.isConfirmed) {
if (allMapDataStore.createFilterId) {
await allMapDataStore.updateFilter();
if(allMapDataStore.isUpdateFilter) {
if (allMapDataStore.isUpdateFilter) {
await savedSuccessfully(allMapDataStore.filterName);
}
} else {
@@ -137,7 +144,7 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
}
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'cancel') {
} else if (result.dismiss === "cancel") {
// console.log('popup cancel case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
@@ -145,18 +152,17 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
allMapDataStore.tempFilterId = null;
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'backdrop') {
} else if (result.dismiss === "backdrop") {
// console.log('popup backdrop case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
pageAdminStore.keepPreviousPage();
if(!logOut){
if (!logOut) {
next(false);
};
}
}
};
}
/**
* Shows a modal dialog to save a new conformance rule with a
* user-provided name.
@@ -166,37 +172,40 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
* @returns {Promise<boolean>} True if the rule was saved, false otherwise.
*/
export async function saveConformance(addConformanceCreateCheckId) {
let fileName = '';
let fileName = "";
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW RULE',
input: 'text',
inputPlaceholder: 'Enter Rule Name.',
title: "SAVE NEW RULE",
input: "text",
inputPlaceholder: "Enter Rule Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addConformanceCreateCheckId(fileName);
// Show save complete notification
if (value) savedSuccessfully(value);
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
return false;
}
}
@@ -212,10 +221,15 @@ export async function saveConformance(addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
export async function leaveConformance(next, addConformanceCreateCheckId, toPath, logOut) {
export async function leaveConformance(
next,
addConformanceCreateCheckId,
toPath,
logOut,
) {
const conformanceStore = useConformanceStore();
const result = await showConfirmationDialog();
if (result.isConfirmed) {
await handleConfirmed(conformanceStore, addConformanceCreateCheckId);
} else {
@@ -228,16 +242,16 @@ export async function leaveConformance(next, addConformanceCreateCheckId, toPath
*/
async function showConfirmationDialog() {
return Swal.fire({
title: 'SAVE YOUR RULE?',
icon: 'warning',
iconColor: '#FF3366',
title: "SAVE YOUR RULE?",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
customClass: customClass
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
}
@@ -248,7 +262,10 @@ async function showConfirmationDialog() {
* @returns {Promise<void>}
*/
async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
if (conformanceStore.conformanceFilterCreateCheckId || conformanceStore.conformanceLogCreateCheckId) {
if (
conformanceStore.conformanceFilterCreateCheckId ||
conformanceStore.conformanceLogCreateCheckId
) {
await conformanceStore.updateConformance();
if (conformanceStore.isUpdateConformance) {
await savedSuccessfully(conformanceStore.conformanceFileName);
@@ -267,13 +284,19 @@ async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
async function handleDismiss(dismissType, conformanceStore, next, toPath, logOut) {
async function handleDismiss(
dismissType,
conformanceStore,
next,
toPath,
logOut,
) {
switch (dismissType) {
case 'cancel':
case "cancel":
resetTempCheckId(conformanceStore);
logOut ? logOut() : next(toPath);
break;
case 'backdrop':
case "backdrop":
if (!logOut) {
next(false);
}
@@ -304,38 +327,38 @@ function resetTempCheckId(conformanceStore) {
export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' | 'not a CSV file'
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' | mime_type
let value = '';
let value = "";
switch (failureType) {
case 'size':
value = 'File size exceeds 90MB.';
case "size":
value = "File size exceeds 90MB.";
break;
case 'encoding':
case "encoding":
value = `Please use UTF-8 for character encoding: (Row #${failureLoc})`;
break;
case 'insufficient_columns':
value = 'Need at least five columns of data.';
case "insufficient_columns":
value = "Need at least five columns of data.";
break;
case 'empty':
value = 'Need at least one record of data.';
case "empty":
value = "Need at least one record of data.";
break;
case 'name_suffix':
case 'mime_type':
value = 'File is not in csv format.';
case "name_suffix":
case "mime_type":
value = "File is not in csv format.";
break;
default:
value = escapeHtml(failureMsg);
break;
}
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: value,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
})
};
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
}
/**
* Shows an error modal for the second stage of upload validation failure,
* listing individual row-level errors.
@@ -345,73 +368,73 @@ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
* @returns {Promise<void>}
*/
export async function uploadFailedSecond(detail) {
let srt = '';
let manySrt = '';
let srt = "";
let manySrt = "";
detail.forEach(i => {
let content = '';
let key = '';
detail.forEach((i) => {
let content = "";
let key = "";
switch (i.type) {
case 'too_many':
manySrt = 'There are more errors.';
case "too_many":
manySrt = "There are more errors.";
break;
case 'unrecognized':
case "unrecognized":
content = `<li>Data unrecognizable in Status Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'malformed':
case "malformed":
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'missing':
case "missing":
switch (i.loc[2]) {
case 'case id':
key = 'Case ID';
case "case id":
key = "Case ID";
break;
case 'timestamp':
key = 'Timestamp';
case "timestamp":
key = "Timestamp";
break;
case 'name':
key = 'Activity';
case "name":
key = "Activity";
break;
case 'instance':
key = 'Activity Instance ID';
case "instance":
key = "Activity Instance ID";
break;
case 'status':
key = 'Status';
case "status":
key = "Status";
break;
default:
key = i.loc[2];
break;
}
content = `<li>Data missing in ${key} Column: (Row #${i.loc[1]})</li>`;
break;
break;
}
srt += content;
});
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
srt = '';
};
srt = "";
}
/**
* Shows a timed success notification after a file upload completes.
* @returns {Promise<void>}
*/
export async function uploadSuccess() {
await Swal.fire({
title: 'IMPORT COMPLETED',
title: "IMPORT COMPLETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows a confirmation dialog before uploading a file. Proceeds with
* the upload only if the user confirms.
@@ -422,23 +445,23 @@ export async function uploadSuccess() {
export async function uploadConfirm(fetchData) {
const filesStore = useFilesStore();
const result = await Swal.fire({
title: 'ARE YOU SURE?',
html: 'After importing, you wont be able to modify labels.',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "ARE YOU SURE?",
html: "After importing, you wont be able to modify labels.",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.uploadLog(fetchData);
}
};
}
/**
* Shows a non-dismissable loading spinner during file upload.
* @returns {Promise<void>}
@@ -449,8 +472,8 @@ export async function uploadloader() {
showConfirmButton: false,
allowOutsideClick: false,
customClass: customClass,
})
};
});
}
/**
* Shows a modal dialog for renaming a file.
*
@@ -463,39 +486,40 @@ export async function uploadloader() {
export async function renameModal(rename, type, id, baseName) {
const fileName = baseName;
const { value, isConfirmed } = await Swal.fire({
title: 'RENAME',
input: 'text',
inputPlaceholder: 'Enter File Name.',
title: "RENAME",
input: "text",
inputPlaceholder: "Enter File Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: '#0099FF',
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
const inputField = Swal.getInput();
inputField.addEventListener('input', function() {
inputField.addEventListener("input", function () {
if (!inputField.value.trim()) {
confirmButton.classList.add('disable-hover');
confirmButton.classList.add("disable-hover");
confirmButton.disabled = true;
} else {
confirmButton.classList.remove('disable-hover');
confirmButton.classList.remove("disable-hover");
confirmButton.disabled = false;
}
});
}
},
});
// Rename succeeded
if(isConfirmed) await rename(type, id, value);
if (isConfirmed) await rename(type, id, value);
// Clear the field: fileName = ''
}
/**
@@ -511,46 +535,50 @@ export async function deleteFileModal(files, type, id, name) {
const filesStore = useFilesStore();
const safeName = escapeHtml(name);
const htmlText = files.length === 0 ? `Do you really want to delete <span class="text-primary">${safeName}</span>?` : `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const htmlText =
files.length === 0
? `Do you really want to delete <span class="text-primary">${safeName}</span>?`
: `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const deleteCustomClass = { ...customClass };
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger";
const result = await Swal.fire({
title: 'CONFIRM DELETION',
title: "CONFIRM DELETION",
html: htmlText,
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonColor: '#ffffff',
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonColor: "#ffffff",
showCancelButton: true,
cancelButtonColor: '#FF3366',
cancelButtonColor: "#FF3366",
customClass: deleteCustomClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #FF3366';
}
confirmButton.style.border = "1px solid #FF3366";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.deleteFile(type, id);
}
};
}
/**
* Shows a timed success notification after file deletion.
* @returns {Promise<void>}
*/
export async function deleteSuccess() {
await Swal.fire({
title: 'FILE(S) DELETED',
title: "FILE(S) DELETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows an informational modal about files deleted by other users,
* then records the deletions and refreshes the file list.
@@ -565,22 +593,25 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
const deleteCustomClass = { ...customClass };
const htmlText = `<div class="text-left mx-4 space-y-1"><p>The following file(s) have been deleted by other user(s):</p><ul class="list-disc ml-6">${files}</ul></div>`;
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary";
deleteCustomClass.cancelButton = null;
await Swal.fire({
title: 'FILE(S) DELETED BY OTHER USER(S)',
title: "FILE(S) DELETED BY OTHER USER(S)",
html: htmlText,
icon: 'info',
iconColor: '#0099FF',
icon: "info",
iconColor: "#0099FF",
customClass: deleteCustomClass,
confirmButtonColor: '#ffffff',
confirmButtonColor: "#ffffff",
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
await Promise.all(reallyDeleteData.map(file => filesStore.deletionRecord(file.id)));
await Promise.all(
reallyDeleteData.map((file) => filesStore.deletionRecord(file.id)),
);
await filesStore.fetchAllFiles();
}
@@ -589,21 +620,21 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
* page with unsaved edits.
* @returns {Promise<void>}
*/
export async function leaveAccountManagementToRemind(){
export async function leaveAccountManagementToRemind() {
const modalStore = useModalStore();
const result = await Swal.fire({
title: 'SAVE YOUR EDIT?',
icon: 'info',
title: "SAVE YOUR EDIT?",
icon: "info",
showCancelButton: true,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
return;
} else {
modalStore.openModal();
}
};
}

View File

@@ -6,8 +6,8 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module apiError Centralized API error handler with toast notifications. */
import {useToast} from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
/**
* Handles API errors by showing a toast notification.
@@ -19,5 +19,5 @@ import 'vue-toast-notification/dist/theme-sugar.css';
*/
export default function apiError(error, toastMessage) {
const $toast = useToast();
$toast.default(toastMessage, {position: 'bottom'});
$toast.default(toastMessage, { position: "bottom" });
}

View File

@@ -9,18 +9,18 @@
* interactive node/edge highlighting, tooltips, and position persistence.
*/
import cytoscape from 'cytoscape';
import spread from 'cytoscape-spread';
import dagre from 'cytoscape-dagre';
import fcose from 'cytoscape-fcose';
import cola from 'cytoscape-cola';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js';
import { createTooltipContent } from '@/module/tooltipContent.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { SAVE_KEY_NAME } from '@/constants/constants.js';
import cytoscape from "cytoscape";
import spread from "cytoscape-spread";
import dagre from "cytoscape-dagre";
import fcose from "cytoscape-fcose";
import cola from "cytoscape-cola";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { useMapPathStore } from "@/stores/mapPathStore";
import { getTimeLabel } from "@/module/timeLabel.js";
import { createTooltipContent } from "@/module/tooltipContent.js";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
import { SAVE_KEY_NAME } from "@/constants/constants.js";
/**
* Composes display text for frequency-type data layer values.
@@ -33,8 +33,14 @@ import { SAVE_KEY_NAME } from '@/constants/constants.js';
*/
const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
let text = baseText;
const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue;
const textFloat = dataLayerOption === 'rel_freq' ? baseText + (optionValue * 100).toFixed(2) + "%" : baseText + optionValue.toFixed(2);
const textInt =
dataLayerOption === "rel_freq"
? baseText + optionValue * 100 + "%"
: baseText + optionValue;
const textFloat =
dataLayerOption === "rel_freq"
? baseText + (optionValue * 100).toFixed(2) + "%"
: baseText + optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
text = Math.trunc(optionValue) === optionValue ? textInt : textFloat;
return text;
@@ -65,7 +71,14 @@ cytoscape.use(cola);
* @param {HTMLElement} graphId - The DOM container element for Cytoscape.
* @returns {cytoscape.Core} The configured Cytoscape instance.
*/
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
export default function cytoscapeMap(
mapData,
dataLayerType,
dataLayerOption,
curveStyle,
rank,
graphId,
) {
// Set the color and style for each node and edge
let nodes = mapData.nodes;
let edges = mapData.edges;
@@ -78,209 +91,234 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
edges: edges, // Edge data
},
layout: {
name: 'dagre',
name: "dagre",
rankDir: rank, // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Style changes when a node is selected
{
selector: 'node:selected',
selector: "node:selected",
style: {
'border-color': 'red',
'border-width': '3',
"border-color": "red",
"border-width": "3",
},
},
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function (node) { // Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = '';
const STRING_LIMIT = 8;
if (node.data('label').length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data('label').substr(0, STRING_LIMIT)}...\n\n`;
} else { // Pad with spaces to match the label width for consistent sizing
text = `${node.data('label').padEnd(STRING_LIMIT, ' ')}\n\n`
label: function (node) {
// Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = "";
const STRING_LIMIT = 8;
if (node.data("label").length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data("label").substr(0, STRING_LIMIT)}...\n\n`;
} else {
// Pad with spaces to match the label width for consistent sizing
text = `${node.data("label").padEnd(STRING_LIMIT, " ")}\n\n`;
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data("type") === "activity") {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case "freq": // Frequency
text = composeFreqTypeText(
text,
dataLayerOption,
optionValue,
);
break;
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
text =
dataLayerOption === "rel_duration"
? textDurRel
: textTimeLabel;
break;
}
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data('type') === 'activity') {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case 'freq': // Frequency
text = composeFreqTypeText(text, dataLayerOption, optionValue);
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
text = dataLayerOption === 'rel_duration' ? textDurRel : textTimeLabel;
break;
}
}
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width':
function (node) {
return node.data('type') === 'activity' ? '1' : '2';
},
'background-image': 'data(nodeImageUrl)',
'background-opacity': 'data(backgroundOpacity)', // Transparent background
'border-opacity': 'data(borderOpacity)', // Transparent border
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 'data(width)', // Wrap text within the node
'text-overflow-wrap': 'anywhere', // Allow wrapping at any position
'text-margin-x': function (node) {
return node.data('type') === 'activity' ? -5 : 0;
return text;
},
'text-margin-y': function (node) {
return node.data('type') === 'activity' ? 2 : 0;
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": function (node) {
return node.data("type") === "activity" ? "1" : "2";
},
'padding': function (node) {
return node.data('type') === 'activity' ? 0 : 0;
"background-image": "data(nodeImageUrl)",
"background-opacity": "data(backgroundOpacity)", // Transparent background
"border-opacity": "data(borderOpacity)", // Transparent border
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": "data(width)", // Wrap text within the node
"text-overflow-wrap": "anywhere", // Allow wrapping at any position
"text-margin-x": function (node) {
return node.data("type") === "activity" ? -5 : 0;
},
"text-margin-y": function (node) {
return node.data("type") === "activity" ? 2 : 0;
},
padding: function (node) {
return node.data("type") === "activity" ? 0 : 0;
},
"text-justification": "left",
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "data(textColor)",
"line-height": "0.7rem",
"font-size": function (node) {
return node.data("type") === "activity" ? 14 : 14;
},
'text-justification': 'left',
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': 'data(textColor)',
'line-height': '0.7rem',
'font-size':
function (node) {
return node.data('type') === 'activity' ? 14 : 14;
},
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'content': function (edge) { // Text displayed on the edge
content: function (edge) {
// Text displayed on the edge
let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
let result = '';
let result = "";
let edgeInt;
let edgeFloat;
let edgeDurRel;
let timeLabelInt;
let timeLabelFloat;
let edgeTimeLabel;
if (optionValue === '') return optionValue;
if (optionValue === "") return optionValue;
switch (dataLayerType) {
case 'freq':
edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue;
edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2);
case "freq":
edgeInt =
dataLayerOption === "rel_freq"
? optionValue * 100 + "%"
: optionValue;
edgeFloat =
dataLayerOption === "rel_freq"
? (optionValue * 100).toFixed(2) + "%"
: optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
result =
Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
edgeDurRel = (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = getTimeLabel(optionValue);
timeLabelFloat = getTimeLabel(optionValue.toFixed(2));
edgeTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
edgeTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
result = dataLayerOption === 'rel_duration' ? edgeDurRel : edgeTimeLabel;
result =
dataLayerOption === "rel_duration"
? edgeDurRel
: edgeTimeLabel;
break;
};
}
return result;
},
'curve-style': curveStyle, // unbundled-bezier | taxi
'overlay-opacity': 0, // Set overlay-opacity to 0 to remove the gray shadow
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
"curve-style": curveStyle, // unbundled-bezier | taxi
"overlay-opacity": 0, // Set overlay-opacity to 0 to remove the gray shadow
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
//'control-point-step-size':100, // Distance between Bezier curve control points
'width': 'data(lineWidth)',
'line-style': 'data(edgeStyle)',
width: "data(lineWidth)",
"line-style": "data(edgeStyle)",
"text-margin-y": "0.7rem",
//"text-rotation": "autorotate",
}
}, {
selector: '.highlight-edge',
style: {
'color': '#0099FF',
'line-color': '#0099FF',
'overlay-color': '#0099FF',
'overlay-opacity': 0.2,
'overlay-padding': '5px',
},
}, {
selector: '.highlight-node',
},
{
selector: ".highlight-edge",
style: {
'overlay-color': '#0099FF',
'overlay-opacity': 0.01,
'overlay-padding': '5px',
color: "#0099FF",
"line-color": "#0099FF",
"overlay-color": "#0099FF",
"overlay-opacity": 0.2,
"overlay-padding": "5px",
},
}, {
selector: 'edge[source = target]', // Select self-loop edges
},
{
selector: ".highlight-node",
style: {
'loop-direction': '0deg', // Control the loop direction
'loop-sweep': '-60deg', // Control the loop arc; adjust to change size
'control-point-step-size': 50 // Control the loop radius; increase to enlarge the loop
}
"overlay-color": "#0099FF",
"overlay-opacity": 0.01,
"overlay-padding": "5px",
},
},
{
selector: "edge[source = target]", // Select self-loop edges
style: {
"loop-direction": "0deg", // Control the loop direction
"loop-sweep": "-60deg", // Control the loop arc; adjust to change size
"control-point-step-size": 50, // Control the loop radius; increase to enlarge the loop
},
},
],
});
// When an edge is clicked, apply glow effect to the edge and its label
cy.on('tap', 'edge', function (event) {
cy.edges().removeClass('highlight-edge');
event.target.addClass('highlight-edge');
cy.on("tap", "edge", function (event) {
cy.edges().removeClass("highlight-edge");
event.target.addClass("highlight-edge");
});
// When a node is clicked, apply glow effect to the node and adjacent edges
cy.on('tap, mousedown', 'node', function (event) {
cy.on("tap, mousedown", "node", function (event) {
useMapPathStore().onNodeClickHighlightEdges(event.target);
});
// When an edge is clicked, apply glow effect to the edge and both endpoint nodes
cy.on('tap, mousedown', 'edge', function (event) {
cy.on("tap, mousedown", "edge", function (event) {
useMapPathStore().onEdgeClickHighlightNodes(event.target);
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function (event) {
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content: content
trigger: "manual",
content: content,
});
if (node.data("label").length > 10) tip.show();
});
cy.on('mouseout', 'node', function (event) {
cy.on("mouseout", "node", function (event) {
tip?.hide();
});
@@ -290,12 +328,20 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
cytoscapeStore.loadPositionsFromStorage(rank);
// Check if localStorage has previously saved visit data.
// If saved node positions exist, restore them for rendering.
if (localStorage.getItem(SAVE_KEY_NAME) && JSON.parse(localStorage.getItem(SAVE_KEY_NAME))) {
const allGraphsRemembered = JSON.parse(localStorage.getItem(SAVE_KEY_NAME));
const currentGraphNodesRemembered =
allGraphsRemembered[cytoscapeStore.currentGraphId] ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank] : null; // May be undefined
if (
localStorage.getItem(SAVE_KEY_NAME) &&
JSON.parse(localStorage.getItem(SAVE_KEY_NAME))
) {
const allGraphsRemembered = JSON.parse(
localStorage.getItem(SAVE_KEY_NAME),
);
const currentGraphNodesRemembered = allGraphsRemembered[
cytoscapeStore.currentGraphId
]
? allGraphsRemembered[cytoscapeStore.currentGraphId][rank]
: null; // May be undefined
if (currentGraphNodesRemembered) {
currentGraphNodesRemembered.forEach(nodeRemembered => {
currentGraphNodesRemembered.forEach((nodeRemembered) => {
const nodeToDecide = cy.getElementById(nodeRemembered.id);
if (nodeToDecide) {
nodeToDecide.position(nodeRemembered.position);
@@ -305,15 +351,23 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
}
// Save the current positions of all nodes when the view is first entered
const allNodes = cy.nodes();
allNodes.forEach(nodeFirstlySave => {
cytoscapeStore.saveNodePosition(nodeFirstlySave.id(), nodeFirstlySave.position(), rank);
allNodes.forEach((nodeFirstlySave) => {
cytoscapeStore.saveNodePosition(
nodeFirstlySave.id(),
nodeFirstlySave.position(),
rank,
);
});
// After node positions change, save the updated positions.
// rank represents whether the user is in horizontal or vertical layout mode.
cy.on('dragfree', 'node', (event) => {
cy.on("dragfree", "node", (event) => {
const nodeToSave = event.target;
cytoscapeStore.saveNodePosition(nodeToSave.id(), nodeToSave.position(), rank);
cytoscapeStore.saveNodePosition(
nodeToSave.id(),
nodeToSave.position(),
rank,
);
});
});

View File

@@ -9,13 +9,13 @@
* individual trace visualization.
*/
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { createTooltipContent } from '@/module/tooltipContent.js';
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { createTooltipContent } from "@/module/tooltipContent.js";
cytoscape.use( dagre );
cytoscape.use(dagre);
/**
* Creates a Cytoscape.js instance for rendering a single trace's
@@ -36,75 +36,79 @@ export default function cytoscapeMapTrace(nodes, edges, graphId) {
edges: edges, // Edge data
},
layout: {
name: 'dagre',
rankDir: 'LR' // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
name: "dagre",
rankDir: "LR", // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function(node) { // Text to display on the node
let text = '';
label: function (node) {
// Text to display on the node
let text = "";
// node.data('label') accesses the original array value at node.data.label
text = node.data('label').length > 18 ? `${node.data('label').substr(0,15)}...` : `${node.data('label')}`;
text =
node.data("label").length > 18
? `${node.data("label").substr(0, 15)}...`
: `${node.data("label")}`;
return text
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width': '1',
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 75,
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': '#001933',
'font-size': 14,
}
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": "1",
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": 75,
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "#001933",
"font-size": 14,
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'curve-style': 'taxi', // unbundled-bezier | taxi
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
'width': 'data(lineWidth)',
'line-style': 'data(style)',
}
"curve-style": "taxi", // unbundled-bezier | taxi
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
width: "data(lineWidth)",
"line-style": "data(style)",
},
},
// Style changes when a node is selected
{
selector: 'node:selected',
style:{
'border-color': 'red',
'border-width': '3',
}
selector: "node:selected",
style: {
"border-color": "red",
"border-width": "3",
},
},
],
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function(event) {
const node = event.target
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content:content
});
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: "manual",
content: content,
});
tip.show();
})
cy.on('mouseout', 'node', function(event) {
});
cy.on("mouseout", "node", function (event) {
tip.hide();
});
}

View File

@@ -12,13 +12,13 @@
* @returns {string} The formatted string with commas (e.g. "1,000,000").
*/
const formatNumberWithCommas = (numberStr) => {
let reversedStr = numberStr.split('').reverse().join('');
let reversedStr = numberStr.split("").reverse().join("");
let groupedStr = reversedStr.match(/.{1,3}/g);
let joinedStr = groupedStr.join(',');
let finalStr = joinedStr.split('').reverse().join('');
let joinedStr = groupedStr.join(",");
let finalStr = joinedStr.split("").reverse().join("");
return finalStr;
}
};
/**
* Converts a number to a string with comma-separated thousands.
@@ -29,7 +29,7 @@ const formatNumberWithCommas = (numberStr) => {
* @returns {string} The formatted number string (e.g. "1,234.56").
*/
export default function numberLabel(num) {
let parts = num.toString().split('.');
let parts = num.toString().split(".");
parts[0] = formatNumberWithCommas(parts[0]);
return parts.join('.');
return parts.join(".");
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module setChartData Chart.js data transformation utilities. */
import getMoment from 'moment';
import getMoment from "moment";
/**
* Extends backend chart data with extrapolated boundary points for
@@ -24,30 +24,30 @@ import getMoment from 'moment';
* with boundary points.
*/
export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) {
// Convert baseData to an array of objects with x and y properties
let data = baseData.map(i => ({ x: i.x, y: i.y }));
// Convert baseData to an array of objects with x and y properties
let data = baseData.map((i) => ({ x: i.x, y: i.y }));
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
};
/**
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
}
/**
* Extrapolates the Y-axis minimum boundary value using linear
* interpolation from the first two data points.
*
@@ -65,7 +65,7 @@ function calculateYMin(baseData, isPercent, yMin, yMax) {
let f = baseData[1].y;
let b = (e * d - a * d - f * a - f * c) / (e - c - a);
return clampValue(b, isPercent, yMin, yMax);
};
}
/**
* Extrapolates the Y-axis maximum boundary value using linear
* interpolation from the last two data points.
@@ -84,7 +84,7 @@ function calculateYMax(baseData, isPercent, yMin, yMax) {
let me = 11;
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
return clampValue(mf, isPercent, yMin, yMax);
};
}
/**
* Clamps a value within a specified range. If isPercent is true, the
* value is clamped to [0, 1]; otherwise to [min, max].
@@ -107,11 +107,11 @@ function clampValue(value, isPercent, min, max) {
return max;
}
if (value <= min) {
return min;
return min;
}
}
return value;
};
}
/**
* Converts backend chart data timestamps to formatted date strings
* for bar charts.
@@ -122,14 +122,14 @@ function clampValue(value, isPercent, min, max) {
* as "YYYY/M/D hh:mm:ss".
*/
export function setBarChartData(baseData) {
let data = baseData.map(i =>{
let data = baseData.map((i) => {
return {
x: getMoment(i.x).format('YYYY/M/D hh:mm:ss'),
y: i.y
}
})
return data
};
x: getMoment(i.x).format("YYYY/M/D hh:mm:ss"),
y: i.y,
};
});
return data;
}
/**
* Divides a time range into evenly spaced time points.
*
@@ -149,9 +149,9 @@ export function timeRange(minTime, maxTime, amount) {
for (let i = 0; i < amount; i++) {
timeRange.push(startTime + timeGap * i);
}
timeRange = timeRange.map(value => Math.round(value));
timeRange = timeRange.map((value) => Math.round(value));
return timeRange;
};
}
/**
* Generates smooth Y-axis values using cubic Bezier interpolation
* to produce evenly spaced ticks matching the X-axis divisions.
@@ -163,39 +163,40 @@ export function timeRange(minTime, maxTime, amount) {
*/
export function yTimeRange(data, yAmount, yMax) {
const yRange = [];
const yGap = (1/ (yAmount-1));
const yGap = 1 / (yAmount - 1);
// Cubic Bezier curve formula
const threebsr = function (t, a1, a2, a3, a4) {
return (
(1 - t) * (1 - t) * (1 - t) * a1 +
3 * t * (1 - t)* (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
)
3 * t * (1 - t) * (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
);
};
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap*11) {
yRange.push(threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y));
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap * 11) {
yRange.push(
threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y),
);
}
}
if(yRange.length < yAmount) {
if (yRange.length < yAmount) {
let len = yAmount - yRange.length;
for (let i = 0; i < len; i++) {
yRange.push(yRange[yRange.length - 1]);
}
}
else if(yRange.length > yAmount) {
} else if (yRange.length > yAmount) {
let len = yRange.length - yAmount;
for(let i = 0; i < len; i++) {
for (let i = 0; i < len; i++) {
yRange.splice(1, 1);
}
}
return yRange;
};
}
/**
* Finds the index of the closest value in an array to the given target.
*
@@ -217,7 +218,7 @@ export function getXIndex(data, xValue) {
}
return closestIndex;
};
}
/**
* Formats a duration in seconds to a compact string with d/h/m/s units.
*
@@ -226,13 +227,13 @@ export function getXIndex(data, xValue) {
* or null if the input is NaN.
*/
export function formatTime(seconds) {
if(!isNaN(seconds)) {
if (!isNaN(seconds)) {
const remainingSeconds = seconds % 60;
const minutes = (Math.floor(seconds - remainingSeconds) / 60) % 60;
const hours = (Math.floor(seconds / 3600)) % 24;
const hours = Math.floor(seconds / 3600) % 24;
const days = Math.floor(seconds / (3600 * 24));
let result = '';
let result = "";
if (days > 0) {
result += `${days}d`;
}
@@ -258,20 +259,20 @@ export function formatTime(seconds) {
export function formatMaxTwo(times) {
const formattedTimes = [];
for (let time of times) {
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = '';
let count = 0;
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = "";
let count = 0;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTime += unit + ' ';
count++;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
formattedTime += unit + " ";
count++;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
}
return formattedTimes;
}

View File

@@ -25,5 +25,5 @@ export default function shortScaleNumber(number) {
index++;
}
num = Math.ceil(num * 10) / 10;
return num + abbreviations[index] + " " ;
return num + abbreviations[index] + " ";
}

View File

@@ -25,7 +25,7 @@ export function sortNumEngZhtw(data) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
});
}
@@ -48,5 +48,5 @@ export function sortNumEngZhtwForFilter(a, b) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module timeLabel Time formatting and chart axis tick utilities. */
import moment from 'moment';
import moment from "moment";
/** @constant {number} Number of decimal places for formatted time values. */
const TOFIXED_DECIMAL = 1;
@@ -20,12 +20,12 @@ const TOFIXED_DECIMAL = 1;
* and the time unit character ("d", "h", "m", or "s").
*/
export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
const {unitToUse, timeValue} = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + (1 / (numOfParts - 1));
const resultStepSize = (timeValue * getLarger / numOfParts);
const { unitToUse, timeValue } = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + 1 / (numOfParts - 1);
const resultStepSize = (timeValue * getLarger) / numOfParts;
return {resultStepSize, unitToUse};
}
return { resultStepSize, unitToUse };
};
/**
* Determines the most appropriate time unit for a given number of seconds
@@ -62,7 +62,7 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
return {
unitToUse: "s",
timeValue: secondToDecide,
}
};
}
};
@@ -75,12 +75,14 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
* @param {string} unitToUse - The time unit suffix ("d", "h", "m", or "s").
* @returns {string} The formatted tick label (e.g. "2.5h").
*/
export function getYTicksByIndex(stepSize, index, unitToUse){
export function getYTicksByIndex(stepSize, index, unitToUse) {
const rawStepsizeMultIndex = (stepSize * index).toString();
const shortenStepsizeMultIndex = rawStepsizeMultIndex.substring(
0, rawStepsizeMultIndex.indexOf('.') + 1 + TOFIXED_DECIMAL);
0,
rawStepsizeMultIndex.indexOf(".") + 1 + TOFIXED_DECIMAL,
);
return `${shortenStepsizeMultIndex}${unitToUse}`;
};
}
/**
* Converts seconds to a human-readable time string with full unit names.
@@ -101,17 +103,15 @@ export function getTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + " days";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + " days";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
if(second == 0){
return second + " sec";
if (second == 0) {
return second + " sec";
}
return second + " sec";
}
@@ -134,17 +134,15 @@ export function simpleTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + "d";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + "d";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + "h";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + "h";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
if(second == 0){
return second + "s";
if (second == 0) {
return second + "s";
}
return second + "s";
}
@@ -167,49 +165,48 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
const dd = max / day;
const hh = max / hour;
const mm = max/ minutes;
let maxUnit = '';
const mm = max / minutes;
let maxUnit = "";
let result = "";
if (dd > 1) {
maxUnit = 'd';
maxUnit = "d";
} else if (hh > 1) {
maxUnit = 'h';
maxUnit = "h";
} else if (mm > 1) {
maxUnit = 'm';
maxUnit = "m";
} else {
maxUnit = 's';
maxUnit = "s";
}
switch (maxUnit) {
case 'd':
if((second / day) === 0) {
case "d":
if (second / day === 0) {
fixedNumber = 0;
}
result = (second / day).toFixed(fixedNumber) + 'd';
result = (second / day).toFixed(fixedNumber) + "d";
break;
case 'h':
if((second / hour) === 0) {
case "h":
if (second / hour === 0) {
fixedNumber = 0;
}
result = (second / hour).toFixed(fixedNumber) + 'h';
result = (second / hour).toFixed(fixedNumber) + "h";
break;
case 'm':
if((second / minutes) === 0) {
case "m":
if (second / minutes === 0) {
fixedNumber = 0;
}
result = (second / minutes).toFixed(fixedNumber) + 'm';
result = (second / minutes).toFixed(fixedNumber) + "m";
break;
case 's':
if(second === 0) {
case "s":
if (second === 0) {
fixedNumber = 0;
}
result = second.toFixed(fixedNumber) + 's';
result = second.toFixed(fixedNumber) + "s";
break;
}
return result;
}
/**
* Selects an appropriate moment.js date format string based on the
* difference between the minimum and maximum timestamps.
@@ -221,20 +218,25 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
* @param {number} maxTimeStamp - The maximum timestamp in seconds.
* @returns {string} A moment.js format string.
*/
export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeStamp) => {
export const setTimeStringFormatBaseOnTimeDifference = (
minTimeStamp,
maxTimeStamp,
) => {
const timeDifferenceInSeconds = maxTimeStamp - minTimeStamp;
let dateFormat;
if (timeDifferenceInSeconds < 60) {
dateFormat = 'HH:mm:ss'; // Seconds range
dateFormat = "HH:mm:ss"; // Seconds range
} else if (timeDifferenceInSeconds < 3600) {
dateFormat = 'MM/DD HH:mm'; // Minutes range
} else if (timeDifferenceInSeconds < 86400) { // 86400 seconds = 24 hours
dateFormat = 'MM/DD HH:mm'; // Hours range
} else if (timeDifferenceInSeconds < 2592000) { // 2592000 seconds = 30 days
dateFormat = 'YYYY/MM/DD'; // Days range
dateFormat = "MM/DD HH:mm"; // Minutes range
} else if (timeDifferenceInSeconds < 86400) {
// 86400 seconds = 24 hours
dateFormat = "MM/DD HH:mm"; // Hours range
} else if (timeDifferenceInSeconds < 2592000) {
// 2592000 seconds = 30 days
dateFormat = "YYYY/MM/DD"; // Days range
} else {
dateFormat = 'YYYY/MM/DD'; // Months range
dateFormat = "YYYY/MM/DD"; // Months range
}
return dateFormat;
@@ -251,6 +253,6 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta
*/
export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => {
if (timeStampArr) {
return timeStampArr.map(ts => moment(ts).format(timeFormat));
}
};
return timeStampArr.map((ts) => moment(ts).format(timeFormat));
}
};

View File

@@ -11,7 +11,7 @@
* @returns {HTMLDivElement} A div element with text-only content.
*/
export function createTooltipContent(label) {
const content = document.createElement('div');
content.textContent = String(label ?? '');
const content = document.createElement("div");
content.textContent = String(label ?? "");
return content;
}

View File

@@ -9,29 +9,29 @@
* navigation guards, and authentication redirect logic.
*/
import { createRouter, createWebHistory, } from "vue-router";
import AuthContainer from '@/views/AuthContainer.vue';
import MainContainer from '@/views/MainContainer.vue';
import Login from '@/views/Login/Login.vue';
import Files from '@/views/Files/Files.vue';
import Upload from '@/views/Upload/index.vue';
import Map from '@/views/Discover/Map/Map.vue';
import Conformance from '@/views/Discover/Conformance/index.vue';
import Performance from '@/views/Discover/Performance/index.vue';
import CompareDashboard from '@/views/Compare/Dashboard/Compare.vue';
import MapCompare from '@/views/Compare/MapCompare.vue';
import AccountAdmin from '@/views/AccountManagement/AccountAdmin/AccountAdmin.vue';
import MyAccount from '@/views/AccountManagement/MyAccount.vue';
import MemberArea from '@/views/MemberArea/index.vue';
import NotFound404 from '@/views/NotFound404.vue';
import { createRouter, createWebHistory } from "vue-router";
import AuthContainer from "@/views/AuthContainer.vue";
import MainContainer from "@/views/MainContainer.vue";
import Login from "@/views/Login/Login.vue";
import Files from "@/views/Files/Files.vue";
import Upload from "@/views/Upload/index.vue";
import Map from "@/views/Discover/Map/Map.vue";
import Conformance from "@/views/Discover/Conformance/index.vue";
import Performance from "@/views/Discover/Performance/index.vue";
import CompareDashboard from "@/views/Compare/Dashboard/Compare.vue";
import MapCompare from "@/views/Compare/MapCompare.vue";
import AccountAdmin from "@/views/AccountManagement/AccountAdmin/AccountAdmin.vue";
import MyAccount from "@/views/AccountManagement/MyAccount.vue";
import MemberArea from "@/views/MemberArea/index.vue";
import NotFound404 from "@/views/NotFound404.vue";
const routes = [
{
path: '/', // Default entry route
redirect: '/files', // Redirect to /files
},
path: "/", // Default entry route
redirect: "/files", // Redirect to /files
},
{
path: '/',
path: "/",
name: "AuthContainer",
component: AuthContainer,
children: [
@@ -40,7 +40,7 @@ const routes = [
name: "Login",
component: Login,
},
]
],
},
{
path: "/",
@@ -48,7 +48,7 @@ const routes = [
component: MainContainer,
meta: {
title: "MainContainer",
requiresAuth: true
requiresAuth: true,
},
children: [
{
@@ -70,16 +70,17 @@ const routes = [
name: "AcctAdmin",
component: AccountAdmin,
},
]
},{
path: "/my-account",
],
},
{
path: "/my-account",
name: "My Account",
component: MyAccount,
},
{
path: "/upload", // router.push({ replace: true }) does not add the path to history
name: "Upload",
component: Upload
component: Upload,
},
{
path: "/discover",
@@ -114,7 +115,7 @@ const routes = [
component: Map,
meta: {
file: {}, // parent log or parent filter
}
},
},
{
// type: log | filter, the parameter can be either.
@@ -124,7 +125,7 @@ const routes = [
component: Conformance,
meta: {
file: {}, // parent log or parent filter
}
},
},
{
// type: log | filter, the parameter can be either.
@@ -134,9 +135,9 @@ const routes = [
component: Performance,
meta: {
file: {}, // parent log or parent filter
}
},
},
]
],
},
{
path: "/compare",
@@ -146,14 +147,15 @@ const routes = [
path: "/compare/dashboard/:primaryType/:primaryId/:secondaryType/:secondaryId",
name: "CompareDashboard",
component: CompareDashboard,
}, {
},
{
path: "/compare/map/:primaryType/:primaryId/:secondaryType/:secondaryId",
name: "MapCompare",
component: MapCompare,
}
]
},
],
},
]
],
},
{
path: "/:pathMatch(.*)*",
@@ -166,7 +168,7 @@ const base_url = import.meta.env.BASE_URL;
const router = createRouter({
history: createWebHistory(base_url), //(/)
// history: createWebHashHistory(base_url), // (/#)
routes
routes,
});
// Global navigation guard
@@ -175,9 +177,11 @@ router.beforeEach((to, from) => {
// from: Route: the current route being navigated away from
// When navigating to the login page, redirect to Files if already logged in
if (to.name === 'Login') {
const isLoggedIn = document.cookie.split(';').some(c => c.trim().startsWith('isLuciaLoggedIn='));
if (isLoggedIn) return { name: 'Files' };
if (to.name === "Login") {
const isLoggedIn = document.cookie
.split(";")
.some((c) => c.trim().startsWith("isLuciaLoggedIn="));
if (isLoggedIn) return { name: "Files" };
}
});

View File

@@ -13,8 +13,8 @@
import { defineStore } from "pinia";
import moment from "moment";
import apiClient from "@/api/client.js";
import apiError from '@/module/apiError.js';
import { Decimal } from 'decimal.js';
import apiError from "@/module/apiError.js";
import { Decimal } from "decimal.js";
/**
* Returns the API base path for the current map data source,
@@ -30,7 +30,7 @@ function getMapApiBase(state) {
}
/** Pinia store for Discover Map page data and filter management. */
export const useAllMapDataStore = defineStore('allMapDataStore', {
export const useAllMapDataStore = defineStore("allMapDataStore", {
state: () => ({
baseLogId: null,
logId: null,
@@ -68,73 +68,85 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
baseInfiniteStart: 0, // Starting index for base infinite scroll cases
}),
getters: {
processMap: state => {
processMap: (state) => {
return state.allProcessMap;
},
bpmn: state => {
bpmn: (state) => {
return state.allBpmn;
},
stats: state => {
stats: (state) => {
return state.allStats;
},
insights: state => {
insights: (state) => {
return state.allInsights;
},
traces: state => {
traces: (state) => {
return state.allTrace.sort((x, y) => x.id - y.id);
},
baseTraces: state => {
baseTraces: (state) => {
return state.allBaseTrace.sort((x, y) => x.id - y.id);
},
cases: state => {
cases: (state) => {
return state.allCase;
},
baseCases: state => {
baseCases: (state) => {
return state.allBaseCase;
},
infiniteFirstCases: state => {
if(state.infiniteStart === 0) return state.allCase;
infiniteFirstCases: (state) => {
if (state.infiniteStart === 0) return state.allCase;
},
BaseInfiniteFirstCases: state => {
if(state.baseInfiniteStart === 0) return state.allBaseCase;
BaseInfiniteFirstCases: (state) => {
if (state.baseInfiniteStart === 0) return state.allBaseCase;
},
traceTaskSeq: state => {
traceTaskSeq: (state) => {
return state.allTraceTaskSeq;
},
baseTraceTaskSeq: state => {
baseTraceTaskSeq: (state) => {
return state.allBaseTraceTaskSeq;
},
// All tasks
filterTasks: state => {
filterTasks: (state) => {
return state.allFilterTask;
},
// form start to end tasks
filterStartToEnd: state => {
filterStartToEnd: (state) => {
return state.allFilterStartToEnd;
},
// form end to start tasks
filterEndToStart: state => {
filterEndToStart: (state) => {
return state.allFilterEndToStart;
},
filterTimeframe: state => {
filterTimeframe: (state) => {
return state.allFilterTimeframe;
},
filterTrace: state => {
filterTrace: (state) => {
return state.allFilterTrace;
},
filterAttrs: state => {
if(state.allFilterAttrs !== null){
return state.allFilterAttrs.map(att => {
filterAttrs: (state) => {
if (state.allFilterAttrs !== null) {
return state.allFilterAttrs.map((att) => {
const copy = { ...att };
switch (copy.type) {
case 'date':
copy.min = copy.min !== null ? moment(copy.min).format('YYYY/MM/DD HH:mm') : null;
copy.max = copy.max !== null ? moment(copy.max).format('YYYY/MM/DD HH:mm') : null;
case "date":
copy.min =
copy.min !== null
? moment(copy.min).format("YYYY/MM/DD HH:mm")
: null;
copy.max =
copy.max !== null
? moment(copy.max).format("YYYY/MM/DD HH:mm")
: null;
break;
case "float":
copy.min =
copy.min !== null
? Number(new Decimal(copy.min).toFixed(2, 1))
: null;
copy.max =
copy.max !== null
? Number(new Decimal(copy.max).toFixed(2, 0))
: null;
break;
case 'float':
copy.min = copy.min !== null ? Number(new Decimal(copy.min).toFixed(2, 1)) : null;
copy.max = copy.max !== null ? Number(new Decimal(copy.max).toFixed(2, 0)) : null;
break
default:
break;
}
@@ -142,7 +154,7 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
});
}
},
allFunnels: state => {
allFunnels: (state) => {
return state.allFunnelData;
},
},
@@ -158,9 +170,9 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
this.allBpmn = response.data.bpmn;
this.allStats = response.data.stats;
this.allInsights = response.data.insights;
} catch(error) {
apiError(error, 'Failed to load the Map.');
};
} catch (error) {
apiError(error, "Failed to load the Map.");
}
},
/**
* fetch trace api.
@@ -173,12 +185,12 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
let baseResponse;
const response = await apiClient.get(api);
this.allTrace = response.data;
if(baseLogId) {
if (baseLogId) {
baseResponse = await apiClient.get(baseApi);
this.allBaseTrace = baseResponse.data;
}
} catch(error) {
apiError(error, 'Failed to load the Trace.');
} catch (error) {
apiError(error, "Failed to load the Trace.");
}
},
/**
@@ -192,30 +204,36 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const response = await apiClient.get(api);
this.allTraceTaskSeq = response.data.task_seq;
this.allCase = response.data.cases;
this.allCase.forEach(c => {
c.started_at = moment(c.started_at).format('YYYY/MM/DD HH:mm');
c.completed_at = moment(c.completed_at).format('YYYY/MM/DD HH:mm');
c.attributes.forEach(att => {
this.allCase.forEach((c) => {
c.started_at = moment(c.started_at).format("YYYY/MM/DD HH:mm");
c.completed_at = moment(c.completed_at).format("YYYY/MM/DD HH:mm");
c.attributes.forEach((att) => {
switch (att.type) {
case 'date':
att.value = att.value !== null ? moment(att.value).format('YYYY/MM/DD HH:mm') : null;
case "date":
att.value =
att.value !== null
? moment(att.value).format("YYYY/MM/DD HH:mm")
: null;
break;
case 'float':
att.value = att.value !== null ? Number(new Decimal(att.value).toFixed(2)) : null;
case "float":
att.value =
att.value !== null
? Number(new Decimal(att.value).toFixed(2))
: null;
break;
default:
break;
}
})
});
});
return this.allCase;
} catch(error) {
if(error.response?.status === 404) {
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, 'Failed to load the Trace Detail.');
};
apiError(error, "Failed to load the Trace Detail.");
}
},
/**
* fetch base log trace detail api.
@@ -230,30 +248,36 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const response = await apiClient.get(api);
this.allBaseTraceTaskSeq = response.data.task_seq;
this.allBaseCase = response.data.cases;
this.allBaseCase.forEach(c => {
c.started_at = moment(c.started_at).format('YYYY/MM/DD HH:mm');
c.completed_at = moment(c.completed_at).format('YYYY/MM/DD HH:mm');
c.attributes.forEach(att => {
this.allBaseCase.forEach((c) => {
c.started_at = moment(c.started_at).format("YYYY/MM/DD HH:mm");
c.completed_at = moment(c.completed_at).format("YYYY/MM/DD HH:mm");
c.attributes.forEach((att) => {
switch (att.type) {
case 'date':
att.value = att.value !== null ? moment(att.value).format('YYYY/MM/DD HH:mm') : null;
case "date":
att.value =
att.value !== null
? moment(att.value).format("YYYY/MM/DD HH:mm")
: null;
break;
case 'float':
att.value = att.value !== null ? Number(new Decimal(att.value).toFixed(2)) : null;
case "float":
att.value =
att.value !== null
? Number(new Decimal(att.value).toFixed(2))
: null;
break;
default:
break;
}
})
});
});
return this.allBaseCase;
} catch(error) {
if(error.response?.status === 404) {
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, 'Failed to load the Base Trace Detail.');
};
apiError(error, "Failed to load the Base Trace Detail.");
}
},
/**
* fetch Filter Parameters api.
@@ -277,11 +301,13 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
this.allFilterTimeframe.x_axis.min_base = min;
this.allFilterTimeframe.x_axis.max_base = max;
// Convert to a time format without seconds
this.allFilterTimeframe.x_axis.min = min !== null ? moment(min).format('YYYY/MM/DD HH:mm') : null;
this.allFilterTimeframe.x_axis.max = max !== null ? moment(max).format('YYYY/MM/DD HH:mm') : null;
} catch(error) {
apiError(error, 'Failed to load the Filter Parameters.');
};
this.allFilterTimeframe.x_axis.min =
min !== null ? moment(min).format("YYYY/MM/DD HH:mm") : null;
this.allFilterTimeframe.x_axis.max =
max !== null ? moment(max).format("YYYY/MM/DD HH:mm") : null;
} catch (error) {
apiError(error, "Failed to load the Filter Parameters.");
}
},
/**
* Test if the Filter Rules Result in Any Data
@@ -291,11 +317,11 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const api = `/api/filters/has-result?log_id=${logId}`;
try {
const response = await apiClient.post(api, this.postRuleData)
const response = await apiClient.post(api, this.postRuleData);
this.hasResultRule = response.data.result;
} catch(error) {
apiError(error, 'Failed to load the Has Result.');
};
} catch (error) {
apiError(error, "Failed to load the Has Result.");
}
},
/**
* Add a New Temporary Filter
@@ -305,11 +331,11 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const api = `/api/temp-filters?log_id=${logId}`;
try {
const response = await apiClient.post(api, this.postRuleData)
const response = await apiClient.post(api, this.postRuleData);
this.tempFilterId = response.data.id;
} catch(error) {
apiError(error, 'Failed to add the Temporary Filters.');
};
} catch (error) {
apiError(error, "Failed to add the Temporary Filters.");
}
},
/**
* Add a New Filter
@@ -320,16 +346,16 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const api = `/api/filters?log_id=${logId}`;
const createFilterObj = {
name: value,
rules: this.postRuleData
rules: this.postRuleData,
};
try {
const response = await apiClient.post(api, createFilterObj);
this.createFilterId = response.data.id;
this.tempFilterId = null;
}catch(error) {
apiError(error, 'Failed to load the Filters.');
};
} catch (error) {
apiError(error, "Failed to load the Filters.");
}
},
/**
* Get Filter Detail
@@ -338,15 +364,15 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
async fetchFunnel(createFilterId) {
const api = `/api/filters/${createFilterId}`;
if(createFilterId){
if (createFilterId) {
try {
const response = await apiClient.get(api);
this.temporaryData = response.data.rules;
this.logId = response.data.log.id;
this.filterName = response.data.name;
this.baseLogId = response.data.log.id;
}catch(error) {
apiError(error, 'Failed to get Filter Detail.');
} catch (error) {
apiError(error, "Failed to get Filter Detail.");
}
}
},
@@ -354,7 +380,7 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
* Update an Existing Filter
*/
async updateFilter() {
const createFilterId = this.createFilterId
const createFilterId = this.createFilterId;
const api = `/api/filters/${createFilterId}`;
const data = this.postRuleData;
@@ -362,9 +388,9 @@ export const useAllMapDataStore = defineStore('allMapDataStore', {
const response = await apiClient.put(api, data);
this.isUpdateFilter = response.status === 200;
this.tempFilterId = null;
}catch(error) {
apiError(error, 'Failed to update an Existing Filter.');
} catch (error) {
apiError(error, "Failed to update an Existing Filter.");
}
},
},
})
});

View File

@@ -11,15 +11,15 @@
import { defineStore } from "pinia";
import apiClient from "@/api/client.js";
import apiError from '@/module/apiError.js';
import apiError from "@/module/apiError.js";
/** Pinia store for the Compare Dashboard page data. */
export const useCompareStore = defineStore('compareStore', {
export const useCompareStore = defineStore("compareStore", {
state: () => ({
allCompareDashboardData: null,
}),
getters: {
compareDashboardData: state => {
compareDashboardData: (state) => {
return state.allCompareDashboardData;
},
},
@@ -36,8 +36,8 @@ export const useCompareStore = defineStore('compareStore', {
try {
const response = await apiClient.get(api);
this.allCompareDashboardData = response.data;
} catch(error) {
apiError(error, 'Failed to load the Compare.');
} catch (error) {
apiError(error, "Failed to load the Compare.");
}
},
/**
@@ -46,38 +46,38 @@ export const useCompareStore = defineStore('compareStore', {
* @param {number} id log or filter ID
*/
async getStateData(type, id) {
let api = '';
let api = "";
switch (type) {
case 'log':
case "log":
api = `/api/logs/${id}/discover`;
break;
case 'filter':
api = `/api/filters/${id}/discover`
case "filter":
api = `/api/filters/${id}/discover`;
break;
}
try {
const response = await apiClient.get(api);
return response.data.stats;
} catch(error) {
} catch (error) {
apiError(error, "Failed to load the Compare's States.");
};
}
},
/**
* Get file's name
* @param {number} id log or filter ID
*/
async getFileName(id) {
id = Number(id)
id = Number(id);
try {
const response = await apiClient.get('/api/files');
const file = response.data.find(i => i.id === id);
const response = await apiClient.get("/api/files");
const file = response.data.find((i) => i.id === id);
if(file) return file.name;
} catch(error) {
if (file) return file.name;
} catch (error) {
apiError(error, "Failed to load the Compare's file name.");
}
}
},
},
})
});

View File

@@ -11,10 +11,10 @@
import { defineStore } from "pinia";
import moment from "moment";
import { Decimal } from 'decimal.js';
import abbreviateNumber from '@/module/abbreviateNumber.js';
import { Decimal } from "decimal.js";
import abbreviateNumber from "@/module/abbreviateNumber.js";
import apiClient from "@/api/client.js";
import apiError from '@/module/apiError.js';
import apiError from "@/module/apiError.js";
/**
* Returns the API base path for the current conformance check,
@@ -23,13 +23,21 @@ import apiError from '@/module/apiError.js';
* @returns {string} The API base path.
*/
function getCheckApiBase(state) {
const { conformanceFilterTempCheckId, conformanceLogTempCheckId,
conformanceFilterCreateCheckId, conformanceLogCreateCheckId } = state;
if (conformanceFilterTempCheckId !== null) return `/api/temp-filter-checks/${conformanceFilterTempCheckId}`;
if (conformanceLogTempCheckId !== null) return `/api/temp-log-checks/${conformanceLogTempCheckId}`;
if (conformanceFilterCreateCheckId !== null) return `/api/filter-checks/${conformanceFilterCreateCheckId}`;
if (conformanceLogCreateCheckId !== null) return `/api/log-checks/${conformanceLogCreateCheckId}`;
return '';
const {
conformanceFilterTempCheckId,
conformanceLogTempCheckId,
conformanceFilterCreateCheckId,
conformanceLogCreateCheckId,
} = state;
if (conformanceFilterTempCheckId !== null)
return `/api/temp-filter-checks/${conformanceFilterTempCheckId}`;
if (conformanceLogTempCheckId !== null)
return `/api/temp-log-checks/${conformanceLogTempCheckId}`;
if (conformanceFilterCreateCheckId !== null)
return `/api/filter-checks/${conformanceFilterCreateCheckId}`;
if (conformanceLogCreateCheckId !== null)
return `/api/log-checks/${conformanceLogCreateCheckId}`;
return "";
}
/**
@@ -41,13 +49,13 @@ function getCheckApiBase(state) {
function getFileTypeApi(state) {
const { conformanceFilterId, conformanceLogId } = state;
if (conformanceFilterId !== null) {
return { prefix: 'filter', idParam: `filter_id=${conformanceFilterId}` };
return { prefix: "filter", idParam: `filter_id=${conformanceFilterId}` };
}
return { prefix: 'log', idParam: `log_id=${conformanceLogId}` };
return { prefix: "log", idParam: `log_id=${conformanceLogId}` };
}
/** Pinia store for conformance checking and rule management. */
export const useConformanceStore = defineStore('conformanceStore', {
export const useConformanceStore = defineStore("conformanceStore", {
state: () => ({
conformanceLogId: null, // Log file
conformanceFilterId: null, // Filter file
@@ -69,12 +77,12 @@ export const useConformanceStore = defineStore('conformanceStore', {
allLoopTraces: null,
allLoopTaskSeq: null,
allLoopCases: null,
selectedRuleType: 'Have activity', // radio
selectedActivitySequence: 'Start & End', // radio
selectedMode: 'Directly follows', // radio
selectedProcessScope: 'End to end', // radio
selectedActSeqMore: 'All', // radio
selectedActSeqFromTo: 'From', // radio
selectedRuleType: "Have activity", // radio
selectedActivitySequence: "Start & End", // radio
selectedMode: "Directly follows", // radio
selectedProcessScope: "End to end", // radio
selectedActSeqMore: "All", // radio
selectedActSeqFromTo: "From", // radio
infinite404: null,
isStartSelected: null, // Whether start is selected in linked start & end selection
isEndSelected: null, // Whether end is selected in linked start & end selection
@@ -83,109 +91,119 @@ export const useConformanceStore = defineStore('conformanceStore', {
conformanceFileName: null, // File name displayed in the save success modal
}),
getters: {
conformanceAllTasks: state => {
conformanceAllTasks: (state) => {
return state.allConformanceTask;
},
conformanceTask: state => {
return state.allConformanceTask.map(i => i.label);
conformanceTask: (state) => {
return state.allConformanceTask.map((i) => i.label);
},
cfmSeqStart: state => {
cfmSeqStart: (state) => {
return state.allCfmSeqStart;
},
cfmSeqEnd: state => {
cfmSeqEnd: (state) => {
return state.allCfmSeqEnd;
},
cfmPtEteWhole: state => {
cfmPtEteWhole: (state) => {
return state.allProcessingTime.end_to_end.whole;
},
cfmPtEteStart: state => {
cfmPtEteStart: (state) => {
return state.allProcessingTime.end_to_end.starts_with;
},
cfmPtEteEnd: state => {
cfmPtEteEnd: (state) => {
return state.allProcessingTime.end_to_end.ends_with;
},
cfmPtEteSE: state => {
cfmPtEteSE: (state) => {
return state.allProcessingTime.end_to_end.start_end;
},
cfmPtPStart: state => {
cfmPtPStart: (state) => {
return state.allProcessingTime.partial.starts_with;
},
cfmPtPEnd: state => {
cfmPtPEnd: (state) => {
return state.allProcessingTime.partial.ends_with;
},
cfmPtPSE: state => {
cfmPtPSE: (state) => {
return state.allProcessingTime.partial.start_end;
},
cfmWtEteWhole: state => {
cfmWtEteWhole: (state) => {
return state.allWaitingTime.end_to_end.whole;
},
cfmWtEteStart: state => {
cfmWtEteStart: (state) => {
return state.allWaitingTime.end_to_end.starts_with;
},
cfmWtEteEnd: state => {
cfmWtEteEnd: (state) => {
return state.allWaitingTime.end_to_end.ends_with;
},
cfmWtEteSE: state => {
cfmWtEteSE: (state) => {
return state.allWaitingTime.end_to_end.start_end;
},
cfmWtPStart: state => {
cfmWtPStart: (state) => {
return state.allWaitingTime.partial.starts_with;
},
cfmWtPEnd: state => {
cfmWtPEnd: (state) => {
return state.allWaitingTime.partial.ends_with;
},
cfmWtPSE: state => {
cfmWtPSE: (state) => {
return state.allWaitingTime.partial.start_end;
},
cfmCtEteWhole: state => {
cfmCtEteWhole: (state) => {
return state.allCycleTime.end_to_end.whole;
},
cfmCtEteStart: state => {
cfmCtEteStart: (state) => {
return state.allCycleTime.end_to_end.starts_with;
},
cfmCtEteEnd: state => {
cfmCtEteEnd: (state) => {
return state.allCycleTime.end_to_end.ends_with;
},
cfmCtEteSE: state => {
cfmCtEteSE: (state) => {
return state.allCycleTime.end_to_end.start_end;
},
conformanceTempReportData: state => {
conformanceTempReportData: (state) => {
return state.allConformanceTempReportData;
},
routeFile: state => {
routeFile: (state) => {
return state.allRouteFile;
},
issueTraces: state => {
issueTraces: (state) => {
return state.allIssueTraces;
},
taskSeq: state => {
taskSeq: (state) => {
return state.allTaskSeq;
},
cases: state => {
if(state.allCases !== null){
return state.allCases.map(c => {
const facets = c.facets.map(fac => {
cases: (state) => {
if (state.allCases !== null) {
return state.allCases.map((c) => {
const facets = c.facets.map((fac) => {
const copy = { ...fac };
switch(copy.type) {
case 'dummy': //sonar-qube
case 'duration-list':
copy.value = copy.value.map(v => v !== null ? abbreviateNumber(new Decimal(v.toFixed(2))) : null);
copy.value = (copy.value).map(v => v.trim()).join(', ');
switch (copy.type) {
case "dummy": //sonar-qube
case "duration-list":
copy.value = copy.value.map((v) =>
v !== null
? abbreviateNumber(new Decimal(v.toFixed(2)))
: null,
);
copy.value = copy.value.map((v) => v.trim()).join(", ");
break;
default:
break;
};
}
return copy;
});
const attributes = c.attributes.map(att => {
const attributes = c.attributes.map((att) => {
const copy = { ...att };
switch (copy.type) {
case 'date':
copy.value = copy.value !== null ? moment(copy.value).format('YYYY/MM/DD HH:mm:ss') : null;
case "date":
copy.value =
copy.value !== null
? moment(copy.value).format("YYYY/MM/DD HH:mm:ss")
: null;
break;
case "float":
copy.value =
copy.value !== null
? new Decimal(copy.value).toFixed(2)
: null;
break;
case 'float':
copy.value = copy.value !== null ? new Decimal(copy.value).toFixed(2) : null;
break
default:
break;
}
@@ -193,32 +211,38 @@ export const useConformanceStore = defineStore('conformanceStore', {
});
return {
...c,
started_at: moment(c.started_at).format('YYYY/MM/DD HH:mm'),
completed_at: moment(c.completed_at).format('YYYY/MM/DD HH:mm'),
started_at: moment(c.started_at).format("YYYY/MM/DD HH:mm"),
completed_at: moment(c.completed_at).format("YYYY/MM/DD HH:mm"),
facets,
attributes,
};
});
};
}
},
loopTraces: state => {
loopTraces: (state) => {
return state.allLoopTraces;
},
loopTaskSeq: state => {
loopTaskSeq: (state) => {
return state.allLoopTaskSeq;
},
loopCases: state => {
if(state.allLoopCases !== null){
return state.allLoopCases.map(c => {
const attributes = c.attributes.map(att => {
loopCases: (state) => {
if (state.allLoopCases !== null) {
return state.allLoopCases.map((c) => {
const attributes = c.attributes.map((att) => {
const copy = { ...att };
switch (copy.type) {
case 'date':
copy.value = copy.value !== null ? moment(copy.value).format('YYYY/MM/DD HH:mm:ss') : null;
case "date":
copy.value =
copy.value !== null
? moment(copy.value).format("YYYY/MM/DD HH:mm:ss")
: null;
break;
case "float":
copy.value =
copy.value !== null
? new Decimal(copy.value).toFixed(2)
: null;
break;
case 'float':
copy.value = copy.value !== null ? new Decimal(copy.value).toFixed(2) : null;
break
default:
break;
}
@@ -226,12 +250,12 @@ export const useConformanceStore = defineStore('conformanceStore', {
});
return {
...c,
started_at: moment(c.started_at).format('YYYY/MM/DD HH:mm'),
completed_at: moment(c.completed_at).format('YYYY/MM/DD HH:mm'),
started_at: moment(c.started_at).format("YYYY/MM/DD HH:mm"),
completed_at: moment(c.completed_at).format("YYYY/MM/DD HH:mm"),
attributes,
};
});
};
}
},
},
actions: {
@@ -249,8 +273,8 @@ export const useConformanceStore = defineStore('conformanceStore', {
this.allProcessingTime = response.data.processing_time;
this.allWaitingTime = response.data.waiting_time;
this.allCycleTime = response.data.cycle_time;
} catch(error) {
apiError(error, 'Failed to load the Conformance Parameters.');
} catch (error) {
apiError(error, "Failed to load the Conformance Parameters.");
}
},
/**
@@ -263,13 +287,13 @@ export const useConformanceStore = defineStore('conformanceStore', {
try {
const response = await apiClient.post(api, data);
if (prefix === 'filter') {
if (prefix === "filter") {
this.conformanceFilterTempCheckId = response.data.id;
} else {
this.conformanceLogTempCheckId = response.data.id;
}
} catch(error) {
apiError(error, 'Failed to add the Temporary Check for a file.');
} catch (error) {
apiError(error, "Failed to add the Temporary Check for a file.");
}
},
/**
@@ -280,13 +304,13 @@ export const useConformanceStore = defineStore('conformanceStore', {
const api = getCheckApiBase(this);
try {
const response = await apiClient.get(api);
if(!getRouteFile) {
this.allConformanceTempReportData = response.data
if (!getRouteFile) {
this.allConformanceTempReportData = response.data;
} else {
this.allRouteFile = response.data.file;
}
} catch(error) {
apiError(error, 'Failed to Get the Temporary Log Conformance Report.');
} catch (error) {
apiError(error, "Failed to Get the Temporary Log Conformance Report.");
}
},
/**
@@ -298,9 +322,12 @@ export const useConformanceStore = defineStore('conformanceStore', {
try {
const response = await apiClient.get(api);
this.allIssueTraces = response.data.traces;
} catch(error) {
apiError(error, 'Failed to Get the detail of a temporary log conformance issue.');
};
} catch (error) {
apiError(
error,
"Failed to Get the detail of a temporary log conformance issue.",
);
}
},
/**
* Get the Trace Details of a Temporary Log Conformance lssue.
@@ -315,13 +342,16 @@ export const useConformanceStore = defineStore('conformanceStore', {
this.allTaskSeq = response.data.task_seq;
this.allCases = response.data.cases;
return response.data.cases;
} catch(error) {
if(error.response?.status === 404) {
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, 'Failed to Get the detail of a temporary log conformance issue.');
};
apiError(
error,
"Failed to Get the detail of a temporary log conformance issue.",
);
}
},
/**
* Get the Details of a Temporary Log Conformance Loop.
@@ -332,9 +362,12 @@ export const useConformanceStore = defineStore('conformanceStore', {
try {
const response = await apiClient.get(api);
this.allLoopTraces = response.data.traces;
} catch(error) {
apiError(error, 'Failed to Get the detail of a temporary log conformance loop.');
};
} catch (error) {
apiError(
error,
"Failed to Get the detail of a temporary log conformance loop.",
);
}
},
/**
* Get the Trace Details of a Temporary Log Conformance Loops.
@@ -349,13 +382,16 @@ export const useConformanceStore = defineStore('conformanceStore', {
this.allLoopTaskSeq = response.data.task_seq;
this.allLoopCases = response.data.cases;
return response.data.cases;
} catch(error) {
if(error.response?.status === 404) {
} catch (error) {
if (error.response?.status === 404) {
this.infinite404 = 404;
return;
}
apiError(error, 'Failed to Get the detail of a temporary log conformance loop.');
};
apiError(
error,
"Failed to Get the detail of a temporary log conformance loop.",
);
}
},
/**
* Add a New Log Conformance Check, Save the log file.
@@ -366,21 +402,21 @@ export const useConformanceStore = defineStore('conformanceStore', {
const api = `/api/${prefix}-checks?${idParam}`;
const data = {
name: value,
rule: this.conformanceRuleData
rule: this.conformanceRuleData,
};
try {
const response = await apiClient.post(api, data);
if (prefix === 'filter') {
if (prefix === "filter") {
this.conformanceFilterCreateCheckId = response.data.id;
this.conformanceFilterTempCheckId = null;
} else {
this.conformanceLogCreateCheckId = response.data.id;
this.conformanceLogTempCheckId = null;
}
}catch(error) {
apiError(error, 'Failed to add the Conformance Check for a file.');
};
} catch (error) {
apiError(error, "Failed to add the Conformance Check for a file.");
}
},
/**
* Update an Existing Conformance Check, log and filter
@@ -394,8 +430,8 @@ export const useConformanceStore = defineStore('conformanceStore', {
this.isUpdateConformance = response.status === 200;
this.conformanceLogTempCheckId = null;
this.conformanceFilterTempCheckId = null;
}catch(error) {
apiError(error, 'Failed to update an Existing Conformance.');
} catch (error) {
apiError(error, "Failed to update an Existing Conformance.");
}
},
/**
@@ -406,4 +442,4 @@ export const useConformanceStore = defineStore('conformanceStore', {
this.conformanceLogCreateCheckId = createCheckID;
},
},
})
});

View File

@@ -12,46 +12,46 @@
*/
import { defineStore } from "pinia";
import moment from 'moment';
import moment from "moment";
/**
* Pinia store for caching user input during conformance rule editing.
*/
export const useConformanceInputStore = defineStore('conformanceInputStore', {
export const useConformanceInputStore = defineStore("conformanceInputStore", {
state: () => ({
inputDataToSave: {
inputStart: null, // TODO: clarify whether "start" means activity start or time start
inputEnd: null,
min: null,
max: null,
type: null,
task: null,
inputStart: null, // TODO: clarify whether "start" means activity start or time start
inputEnd: null,
min: null,
max: null,
type: null,
task: null,
},
activityRadioData: {
category: null,
task: ['', ''],
task: ["", ""],
},
}),
getters: {
},
getters: {},
actions: {
/**
* Sets the activity radio global state for either the start ('From') or end ('To') of a task.
* We arrange in this way because backend needs us to communicate in this way.
* @param {object} actRadioData
* @param {object} actRadioData
* @param {string} fromToStr 'From' or 'To'
*/
setActivityRadioStartEndData(actRadioData, fromToStr){
switch(fromToStr) {
case 'From':
setActivityRadioStartEndData(actRadioData, fromToStr) {
switch (fromToStr) {
case "From":
this.activityRadioData.task[0] = actRadioData;
break;
case 'To':
this.activityRadioData.task[this.activityRadioData.task.length - 1] = actRadioData;
case "To":
this.activityRadioData.task[this.activityRadioData.task.length - 1] =
actRadioData;
break;
default:
break;
}
},
},
})
});

View File

@@ -11,18 +11,24 @@
import { defineStore } from "pinia";
import apiClient from "@/api/client.js";
import moment from 'moment';
import apiError from '@/module/apiError.js';
import Swal from 'sweetalert2';
import { uploadFailedFirst, uploadFailedSecond, uploadloader, uploadSuccess, deleteSuccess } from '@/module/alertModal.js';
import { useLoadingStore } from '@/stores/loading';
import moment from "moment";
import apiError from "@/module/apiError.js";
import Swal from "sweetalert2";
import {
uploadFailedFirst,
uploadFailedSecond,
uploadloader,
uploadSuccess,
deleteSuccess,
} from "@/module/alertModal.js";
import { useLoadingStore } from "@/stores/loading";
/** Map of file type to API path segment. */
const FILE_TYPE_PATHS = {
'log': 'logs',
'filter': 'filters',
'log-check': 'log-checks',
'filter-check': 'filter-checks',
log: "logs",
filter: "filters",
"log-check": "log-checks",
"filter-check": "filter-checks",
};
/**
@@ -36,22 +42,22 @@ function getFileApiBase(type, id) {
}
/** Pinia store for file CRUD operations and upload workflow. */
export const useFilesStore = defineStore('filesStore', {
export const useFilesStore = defineStore("filesStore", {
state: () => ({
allEventFiles: [
{
parentLog: '',
fileType: '',
ownerName: '',
}
parentLog: "",
fileType: "",
ownerName: "",
},
],
switchFilesTagData: {
ALL: ['Log', 'Filter', 'Rule', 'Design'],
DISCOVER: ['Log', 'Filter', 'Rule'],
COMPARE: ['Log','Filter'],
DESIGN: ['Log', 'Design'],
ALL: ["Log", "Filter", "Rule", "Design"],
DISCOVER: ["Log", "Filter", "Rule"],
COMPARE: ["Log", "Filter"],
DESIGN: ["Log", "Design"],
},
filesTag: 'ALL',
filesTag: "ALL",
httpStatus: 200,
uploadId: null,
allUploadDetail: null,
@@ -63,64 +69,64 @@ export const useFilesStore = defineStore('filesStore', {
/**
* Get allFiles and switch files tag
*/
allFiles: state => {
allFiles: (state) => {
let result = state.allEventFiles;
const data = state.switchFilesTagData;
const filesTag = state.filesTag;
result = result.filter(file => data[filesTag].includes(file.fileType));
result = result.filter((file) => data[filesTag].includes(file.fileType));
return result;
},
/**
* Get upload preview
*/
uploadDetail: state => {
uploadDetail: (state) => {
return state.allUploadDetail;
},
/**
* Get dependents of files data
*/
dependentsData: state => {
dependentsData: (state) => {
return state.allDependentsData;
}
},
},
actions: {
/**
* Fetch All Files api
*/
async fetchAllFiles() {
const api = '/api/files';
const api = "/api/files";
try {
const response = await apiClient.get(api);
this.allEventFiles = response.data;
this.allEventFiles.forEach(o => {
let icon = '';
let fileType = '';
this.allEventFiles.forEach((o) => {
let icon = "";
let fileType = "";
let parentLog = o.name;
switch (o.type) {
case 'log':
icon = 'work_history';
fileType = 'Log';
case "log":
icon = "work_history";
fileType = "Log";
parentLog = o.name;
break;
case 'filter':
icon = 'tornado';
fileType = 'Filter';
case "filter":
icon = "tornado";
fileType = "Filter";
parentLog = o.parent.name;
break;
case 'log-check':
case 'filter-check':
icon = 'local_police';
fileType = 'Rule';
case "log-check":
case "filter-check":
icon = "local_police";
fileType = "Rule";
parentLog = o.parent.name;
break;
case 'design':
icon = 'shape_line';
fileType = 'Design';
case "design":
icon = "shape_line";
fileType = "Design";
parentLog = o.name;
break;
}
@@ -130,23 +136,29 @@ export const useFilesStore = defineStore('filesStore', {
o.ownerName = o.owner.name;
o.updated_base = o.updated_at;
o.accessed_base = o.accessed_at;
o.updated_at = moment(o.updated_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm');
o.accessed_at = o.accessed_at ? moment(o.accessed_at).utcOffset('+08:00').format('YYYY-MM-DD HH:mm') : null;
o.updated_at = moment(o.updated_at)
.utcOffset("+08:00")
.format("YYYY-MM-DD HH:mm");
o.accessed_at = o.accessed_at
? moment(o.accessed_at)
.utcOffset("+08:00")
.format("YYYY-MM-DD HH:mm")
: null;
});
} catch(error) {
apiError(error, 'Failed to load the files.');
};
} catch (error) {
apiError(error, "Failed to load the files.");
}
},
/**
* Uploads a CSV log file (first stage).
* @param {Object} fromData - The form data to send to the backend.
*/
async upload(fromData) {
const api = '/api/logs/csv-uploads';
const api = "/api/logs/csv-uploads";
const config = {
data: true,
headers: {
'Content-Type': 'multipart/form-data',
"Content-Type": "multipart/form-data",
},
};
@@ -155,10 +167,10 @@ export const useFilesStore = defineStore('filesStore', {
const response = await apiClient.post(api, fromData, config);
this.uploadId = response.data.id;
this.$router.push({name: 'Upload'});
this.$router.push({ name: "Upload" });
Swal.close(); // Close the loading progress bar
} catch(error) {
if(error.response?.status === 422) {
} catch (error) {
if (error.response?.status === 422) {
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv'
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix'
const detail = error.response.data.detail;
@@ -166,7 +178,7 @@ export const useFilesStore = defineStore('filesStore', {
uploadFailedFirst(detail[0].type, detail[0].msg, detail[0].loc[2]);
} else {
Swal.close(); // Close the loading progress bar
apiError(error, 'Failed to upload the files.');
apiError(error, "Failed to upload the files.");
}
}
},
@@ -180,8 +192,8 @@ export const useFilesStore = defineStore('filesStore', {
try {
const response = await apiClient.get(api);
this.allUploadDetail = response.data.preview;
} catch(error) {
apiError(error, 'Failed to get upload detail.');
} catch (error) {
apiError(error, "Failed to get upload detail.");
}
},
/**
@@ -200,15 +212,15 @@ export const useFilesStore = defineStore('filesStore', {
Swal.close(); // Close the loading progress bar
await this.rename(); // Rename the file
await uploadSuccess();
this.$router.push({name: 'Files'});
} catch(error) {
if(error.response?.status === 422) {
this.$router.push({ name: "Files" });
} catch (error) {
if (error.response?.status === 422) {
const detail = [...error.response.data.detail];
uploadFailedSecond(detail);
} else {
Swal.close(); // Close the loading progress bar
apiError(error, 'Failed to upload the log files.');
apiError(error, "Failed to upload the log files.");
}
}
},
@@ -220,19 +232,19 @@ export const useFilesStore = defineStore('filesStore', {
*/
async rename(type, id, fileName) {
// If uploadLogId exists, set id and type accordingly; then check the file type.
if(this.uploadId && this.uploadFileName) {
type = 'log';
if (this.uploadId && this.uploadFileName) {
type = "log";
id = this.uploadLogId;
fileName = this.uploadFileName;
}
const data = {"name": fileName};
const data = { name: fileName };
try {
await apiClient.put(`${getFileApiBase(type, id)}/name`, data);
this.uploadFileName = null;
await this.fetchAllFiles();
} catch(error) {
apiError(error, 'Failed to rename.');
} catch (error) {
apiError(error, "Failed to rename.");
}
},
/**
@@ -242,10 +254,12 @@ export const useFilesStore = defineStore('filesStore', {
*/
async getDependents(type, id) {
try {
const response = await apiClient.get(`${getFileApiBase(type, id)}/dependents`);
const response = await apiClient.get(
`${getFileApiBase(type, id)}/dependents`,
);
this.allDependentsData = response.data;
} catch(error) {
apiError(error, 'Failed to get Dependents of the files.');
} catch (error) {
apiError(error, "Failed to get Dependents of the files.");
}
},
/**
@@ -254,18 +268,18 @@ export const useFilesStore = defineStore('filesStore', {
* @param {number} id - The file ID.
*/
async deleteFile(type, id) {
if(id === null || id === undefined || isNaN(id)) {
console.error('Delete File API Error: invalid id');
if (id === null || id === undefined || isNaN(id)) {
console.error("Delete File API Error: invalid id");
return;
};
}
const loading = useLoadingStore();
loading.isLoading = true;
try {
await apiClient.delete(getFileApiBase(type, id));
await this.fetchAllFiles();
await deleteSuccess();
} catch(error) {
apiError(error, 'Failed to delete.');
} catch (error) {
apiError(error, "Failed to delete.");
} finally {
loading.isLoading = false;
}
@@ -275,15 +289,15 @@ export const useFilesStore = defineStore('filesStore', {
* @param {number} id - The file ID.
*/
async deletionRecord(id) {
let api = '';
let api = "";
const loading = useLoadingStore();
loading.isLoading = true;
api = `/api/deletion/${id}`;
try {
await apiClient.delete(api);
} catch(error) {
apiError(error, 'Failed to Remove a Deletion Record.')
} catch (error) {
apiError(error, "Failed to Remove a Deletion Record.");
} finally {
loading.isLoading = false;
}
@@ -295,21 +309,21 @@ export const useFilesStore = defineStore('filesStore', {
* @param {string} fileName - The file name.
*/
async downloadFileCSV(type, id, fileName) {
if (type !== 'log' && type !== 'filter') return;
if (type !== "log" && type !== "filter") return;
try {
const response = await apiClient.get(`${getFileApiBase(type, id)}/csv`);
const csvData = response.data;
const blob = new Blob([csvData], { type: 'text/csv' });
const blob = new Blob([csvData], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = `${fileName}.csv`;
link.click();
window.URL.revokeObjectURL(url);
} catch(error) {
apiError(error, 'Failed to download.');
} catch (error) {
apiError(error, "Failed to download.");
}
},
},
})
});

View File

@@ -9,7 +9,7 @@
import { defineStore } from "pinia";
/** Pinia store for managing the global loading spinner visibility. */
export const useLoadingStore = defineStore('loadingStore', {
export const useLoadingStore = defineStore("loadingStore", {
state: () => ({
/** Whether the loading spinner is currently visible. */
isLoading: true,
@@ -21,6 +21,6 @@ export const useLoadingStore = defineStore('loadingStore', {
*/
setIsLoading(isLoadingBoolean) {
this.isLoading = isLoadingBoolean;
}
},
},
});

View File

@@ -10,20 +10,24 @@
*/
import { defineStore } from "pinia";
import axios from 'axios';
import apiClient from '@/api/client.js';
import apiError from '@/module/apiError.js';
import { deleteCookie, setCookie, setCookieWithoutExpiration } from "../utils/cookieUtil";
import axios from "axios";
import apiClient from "@/api/client.js";
import apiError from "@/module/apiError.js";
import {
deleteCookie,
setCookie,
setCookieWithoutExpiration,
} from "../utils/cookieUtil";
/** Pinia store for authentication and user session management. */
export const useLoginStore = defineStore('loginStore', {
export const useLoginStore = defineStore("loginStore", {
// data, methods, computed
// state, actions, getters
state: () => ({
auth: {
grant_type: 'password', // password | refresh_token
username: '',
password: '',
grant_type: "password", // password | refresh_token
username: "",
password: "",
refresh_token: undefined,
},
isInvalid: false,
@@ -36,11 +40,11 @@ export const useLoginStore = defineStore('loginStore', {
* fetch Login For Access Token api
*/
async signIn() {
const api = '/api/oauth/token';
const api = "/api/oauth/token";
const config = {
headers: {
// Default URL-encoded format for HTTP POST, not JSON
'Content-Type':'application/x-www-form-urlencoded',
"Content-Type": "application/x-www-form-urlencoded",
},
};
@@ -52,7 +56,13 @@ export const useLoginStore = defineStore('loginStore', {
setCookieWithoutExpiration("luciaToken", accessToken);
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + 6);
setCookie("luciaRefreshToken", refresh_token, Math.ceil((expiryDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000)));
setCookie(
"luciaRefreshToken",
refresh_token,
Math.ceil(
(expiryDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000),
),
);
this.isLoggedIn = true;
setCookie("isLuciaLoggedIn", "true");
@@ -60,30 +70,30 @@ export const useLoginStore = defineStore('loginStore', {
// By default, redirect to the FILES page.
// However, if the user pasted a URL while not logged in,
// redirect them to the remembered return-to URL after login.
if(this.rememberedReturnToUrl !== "") {
if (this.rememberedReturnToUrl !== "") {
const decodedUrl = atob(this.rememberedReturnToUrl);
// Only allow relative paths to prevent open redirect attacks
if(decodedUrl.startsWith('/') && !decodedUrl.startsWith('//')) {
if (decodedUrl.startsWith("/") && !decodedUrl.startsWith("//")) {
window.location.href = decodedUrl;
} else {
this.$router.push('/files');
this.$router.push("/files");
}
} else {
this.$router.push('/files');
this.$router.push("/files");
}
} catch(error) {
} catch (error) {
this.isInvalid = true;
};
}
},
/**
* Refresh Token
*/
async refreshToken() {
try {
const { refreshTokenAndGetNew } = await import('@/api/auth.js');
const { refreshTokenAndGetNew } = await import("@/api/auth.js");
await refreshTokenAndGetNew();
} catch(error) {
this.$router.push('/login');
} catch (error) {
this.$router.push("/login");
throw error;
}
},
@@ -96,36 +106,36 @@ export const useLoginStore = defineStore('loginStore', {
this.isLoggedIn = false;
deleteCookie("isLuciaLoggedIn");
this.$router.push('/login');
this.$router.push("/login");
},
/**
* get user detail for 'my-account' api
*/
async getUserData() {
const api = '/api/my-account';
const api = "/api/my-account";
try {
const response = await apiClient.get(api);
this.userData = response.data;
} catch(error) {
apiError(error, 'Failed to load user data.');
};
} catch (error) {
apiError(error, "Failed to load user data.");
}
},
/**
* check login for 'my-account' api
*/
async checkLogin() {
const api = '/api/my-account';
const api = "/api/my-account";
try {
await apiClient.get(api);
} catch(error) {
this.$router.push('/login');
};
} catch (error) {
this.$router.push("/login");
}
},
setRememberedReturnToUrl(returnToUrl){
this.rememberedReturnToUrl = returnToUrl
setRememberedReturnToUrl(returnToUrl) {
this.rememberedReturnToUrl = returnToUrl;
},
setIsLoggedIn(boolean) {
this.isLoggedIn = boolean;

View File

@@ -8,8 +8,8 @@
* @module stores/main Pinia store instance with persisted state plugin.
*/
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

View File

@@ -6,11 +6,11 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module stores/modal Account management modal state. */
import { defineStore } from 'pinia';
import { MODAL_ACCT_INFO, } from '@/constants/constants.js';
import { defineStore } from "pinia";
import { MODAL_ACCT_INFO } from "@/constants/constants.js";
/** Pinia store for controlling account management modal visibility. */
export const useModalStore = defineStore('modalStore', {
export const useModalStore = defineStore("modalStore", {
state: () => ({
/** Whether a modal is currently open. */
isModalOpen: false,
@@ -27,7 +27,7 @@ export const useModalStore = defineStore('modalStore', {
this.whichModal = whichModal;
},
/** Closes the currently open modal. */
async closeModal(){
async closeModal() {
this.isModalOpen = false;
},
},

View File

@@ -23,56 +23,65 @@ const printPageAdminLog = false;
* page refreshes. Manages pending states for SweetAlert2 modal
* confirmation flows.
*/
export const usePageAdminStore = defineStore('pageAdminStore', {
export const usePageAdminStore = defineStore("pageAdminStore", {
state: () => ({
activePage: 'MAP',
previousPage: 'MAP',
pendingActivePage: 'FILES',
isPagePending: false,
shouldKeepPreviousPage: false, // false -- meaning modal is not pressed as "NO"
activePageComputedByRoute: "MAP",
currentMapFile: '',
activePage: "MAP",
previousPage: "MAP",
pendingActivePage: "FILES",
isPagePending: false,
shouldKeepPreviousPage: false, // false -- meaning modal is not pressed as "NO"
activePageComputedByRoute: "MAP",
currentMapFile: "",
}),
getters: {
},
getters: {},
actions: {
/**
* Sets the active page based on the last matched route record.
* @param {Array} routeMatched - The route.matched array.
*/
setActivePageComputedByRoute(routeMatched){
if (routeMatched.length && routeMatched[routeMatched.length - 1]
&& routeMatched[routeMatched.length - 1].name) {
printPageAdminLog && console.log('setActivePageComputedByRoute()', mapPageNameToCapitalUnifiedName(routeMatched[routeMatched.length - 1].name));
setActivePageComputedByRoute(routeMatched) {
if (
routeMatched.length &&
routeMatched[routeMatched.length - 1] &&
routeMatched[routeMatched.length - 1].name
) {
printPageAdminLog &&
console.log(
"setActivePageComputedByRoute()",
mapPageNameToCapitalUnifiedName(
routeMatched[routeMatched.length - 1].name,
),
);
this.activePageComputedByRoute = mapPageNameToCapitalUnifiedName(
routeMatched[routeMatched.length - 1].name);
routeMatched[routeMatched.length - 1].name,
);
}
},
/**
* Set Active Page; the page which the user is currently visiting
* @param {string} activePage
* @param {string} activePage
*/
setActivePage(activePage) {
printPageAdminLog && console.log("setActivePage", activePage)
printPageAdminLog && console.log("setActivePage", activePage);
this.activePage = mapPageNameToCapitalUnifiedName(activePage);
},
/**
* Specify previous page state value.
* @param {string} prevPage
* @param {string} prevPage
*/
setPreviousPage(prevPage) {
this.previousPage = mapPageNameToCapitalUnifiedName(prevPage);
},
/**
* Set the previous(usually current) pages to the ones we just decide.
*/
* Set the previous(usually current) pages to the ones we just decide.
*/
setPreviousPageUsingActivePage() {
this.previousPage = this.activePage;
},
/**
* Set the boolean value of status of pending state of the pate
* For the control of Swal popup
* @param {boolean} boolVal
* @param {boolean} boolVal
*/
setIsPagePendingBoolean(boolVal) {
this.isPagePending = boolVal;
@@ -81,49 +90,59 @@ export const usePageAdminStore = defineStore('pageAdminStore', {
* Copy(transit) the value of pendingActivePage to activePage
*/
copyPendingPageToActivePage() {
printPageAdminLog && console.log('pinia copying this.pendingActivePage', this.pendingActivePage);
printPageAdminLog &&
console.log(
"pinia copying this.pendingActivePage",
this.pendingActivePage,
);
this.activePage = this.pendingActivePage;
},
},
/**
* Set Pending active Page, meaning we are not sure if user will chang her mind later on.
* Also, start pending state.
* Often, user triggers the modal and the pending state starts.
* Note: String conversion is needed. For Example, CheckMap is converted into MAP
* @param {string} pendingActivePage
* @param {string} pendingActivePage
*/
setPendingActivePage(argPendingActivePage) {
printPageAdminLog && console.log('pinia setting this.pendingActivePage', this.pendingActivePage);
this.pendingActivePage = mapPageNameToCapitalUnifiedName(argPendingActivePage);
printPageAdminLog &&
console.log(
"pinia setting this.pendingActivePage",
this.pendingActivePage,
);
this.pendingActivePage =
mapPageNameToCapitalUnifiedName(argPendingActivePage);
},
/**
* Set Pending active Page to empty string; we call this right after we just decide an active page.
* Also, stop pending state.
*/
clearPendingActivePage(){
this.pendingActivePage = '';
clearPendingActivePage() {
this.pendingActivePage = "";
},
/**
* When user dismiss the popup modal, we don't apply the new page,
* instead, we apply the previous page.
*/
keepPreviousPage() {
printPageAdminLog && console.log('pinia keeping this.previousPage', this.previousPage);
printPageAdminLog &&
console.log("pinia keeping this.previousPage", this.previousPage);
this.activePage = this.previousPage;
this.shouldKeepPreviousPage = true;
},
/**
* Clean up the state of the boolean related to modal showing phase
*/
clearShouldKeepPreviousPageBoolean(){
printPageAdminLog && console.log('clearShouldKeepPreviousPageBoolean()');
clearShouldKeepPreviousPageBoolean() {
printPageAdminLog && console.log("clearShouldKeepPreviousPageBoolean()");
this.shouldKeepPreviousPage = false;
},
/**
* Stores the name of the currently opened map file.
* @param {string} fileName - The file name.
*/
setCurrentMapFile(fileName){
setCurrentMapFile(fileName) {
this.currentMapFile = fileName;
},
},
})
});

View File

@@ -11,10 +11,10 @@
import { defineStore } from "pinia";
import apiClient from "@/api/client.js";
import apiError from '@/module/apiError.js';
import apiError from "@/module/apiError.js";
/** Pinia store for the Discover Performance page data. */
export const usePerformanceStore = defineStore('performanceStore', {
export const usePerformanceStore = defineStore("performanceStore", {
state: () => ({
allPerformanceData: null,
freqChartData: null,
@@ -24,10 +24,10 @@ export const usePerformanceStore = defineStore('performanceStore', {
maxX: null,
xData: null,
content: null,
}
},
}),
getters: {
performanceData: state => {
performanceData: (state) => {
return state.allPerformanceData;
},
},
@@ -38,43 +38,43 @@ export const usePerformanceStore = defineStore('performanceStore', {
* @param {number} id - The file ID.
*/
async getPerformance(type, id) {
let api = '';
let api = "";
switch (type) {
case 'log':
case "log":
api = `/api/logs/${id}/performance`;
break;
case 'filter':
case "filter":
api = `/api/filters/${id}/performance`;
break;
}
try {
const response = await apiClient.get(api);
this.allPerformanceData = response.data;
} catch(error) {
apiError(error, 'Failed to load the Performance.');
} catch (error) {
apiError(error, "Failed to load the Performance.");
}
},
/**
* In PrimeVue format
* @param {object} freqChartData
* @param {object} freqChartData
*/
setFreqChartData(freqChartData){
setFreqChartData(freqChartData) {
this.freqChartData = freqChartData;
},
/**
* In PrimeVue format
* @param {object} freqChartOptions
* @param {object} freqChartOptions
*/
setFreqChartOptions(freqChartOptions){
setFreqChartOptions(freqChartOptions) {
this.freqChartOptions = freqChartOptions;
},
/**
*
* @param {object} freqChartXData
*
* @param {object} freqChartXData
*/
setFreqChartXData(freqChartXData) {
this.freqChartXData = freqChartXData;
}
},
},
})
});

2
src/types/vue.d.ts vendored
View File

@@ -8,4 +8,4 @@
* Vue App type import (placeholder).
*/
import { App } from 'vue';
import { App } from "vue";

View File

@@ -13,7 +13,7 @@
*/
export function getCookie(name) {
const nameEqual = name + "=";
const cookieArr = document.cookie.split(';');
const cookieArr = document.cookie.split(";");
for (const cookie of cookieArr) {
let c = cookie.trim();
@@ -34,14 +34,15 @@ export function getCookie(name) {
* @param {string} value - The cookie value.
* @param {number} [days=1] - Number of days until the cookie expires.
*/
export function setCookie(name, value, days=1) {
export function setCookie(name, value, days = 1) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/; Secure; SameSite=Lax";
document.cookie =
name + "=" + (value || "") + expires + "; path=/; Secure; SameSite=Lax";
}
/**
@@ -50,7 +51,8 @@ export function setCookie(name, value, days=1) {
* @param {string} value - The cookie value.
*/
export function setCookieWithoutExpiration(name, value) {
document.cookie = name + "=" + (value || "") + "; path=/; Secure; SameSite=Lax";
document.cookie =
name + "=" + (value || "") + "; path=/; Secure; SameSite=Lax";
}
/**
@@ -58,6 +60,7 @@ export function setCookieWithoutExpiration(name, value) {
* @param {string} name - The cookie name to delete.
* @param {string} [path='/'] - The path scope of the cookie.
*/
export function deleteCookie(name, path = '/') {
document.cookie = name + '=; Max-Age=-99999999; path=' + path + '; Secure; SameSite=Lax';
export function deleteCookie(name, path = "/") {
document.cookie =
name + "=; Max-Age=-99999999; path=" + path + "; Secure; SameSite=Lax";
}

View File

@@ -4,7 +4,7 @@
// imacat.yang@dsp.im (imacat), 2023/9/23
/** @module emitter Shared mitt event bus instance. */
import mitt from 'mitt';
import mitt from "mitt";
/** Global event emitter for cross-component communication. */
const emitter = mitt();

View File

@@ -12,9 +12,9 @@
*/
export function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

View File

@@ -12,20 +12,20 @@
* @param {number} [indent=0] - The current indentation level.
*/
export const printObject = (obj, indent = 0) => {
const padding = ' '.repeat(indent);
const padding = " ".repeat(indent);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
console.log(`${padding}${key}: {`);
printObject(obj[key], indent + 2);
console.log(`${padding}}`);
} else {
console.log(`${padding}${key}: ${obj[key]}`);
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === "object" && obj[key] !== null) {
console.log(`${padding}${key}: {`);
printObject(obj[key], indent + 2);
console.log(`${padding}}`);
} else {
console.log(`${padding}${key}: ${obj[key]}`);
}
}
};
}
};
/**
* Returns a cryptographically random integer between 0 and max (inclusive).
@@ -35,5 +35,5 @@ export const printObject = (obj, indent = 0) => {
export const getRandomInt = (max) => {
const array = new Uint32Array(1);
window.crypto.getRandomValues(array);
return Math.floor(array[0] / (0xFFFFFFFF + 1) * (max + 1));
return Math.floor((array[0] / (0xffffffff + 1)) * (max + 1));
};

View File

@@ -18,23 +18,20 @@
* undefined if rawPageName is falsy.
*/
const mapPageNameToCapitalUnifiedName = (rawPageName) => {
if(rawPageName) {
switch (rawPageName.toUpperCase()) {
case 'CHECKMAP':
return 'MAP';
case 'CHECKCONFORMANCE':
return 'CONFORMANCE';
case 'CHECKPERFORMANCE':
return 'PERFORMANCE';
case 'COMPAREDASHBOARD':
return 'DASHBOARD';
default:
return rawPageName.toUpperCase();
}
if (rawPageName) {
switch (rawPageName.toUpperCase()) {
case "CHECKMAP":
return "MAP";
case "CHECKCONFORMANCE":
return "CONFORMANCE";
case "CHECKPERFORMANCE":
return "PERFORMANCE";
case "COMPAREDASHBOARD":
return "DASHBOARD";
default:
return rawPageName.toUpperCase();
}
}
};
export {
mapPageNameToCapitalUnifiedName,
};
export { mapPageNameToCapitalUnifiedName };

View File

@@ -11,7 +11,6 @@
* @param {number} [s=2.5] - The delay in seconds.
* @returns {Promise<void>} A promise that resolves after the delay.
*/
export const delaySecond = (s = 2.5) => {
return new Promise((resolve) =>
setTimeout(resolve, s * 1000)
);}
export const delaySecond = (s = 2.5) => {
return new Promise((resolve) => setTimeout(resolve, s * 1000));
};

View File

@@ -1,115 +1,239 @@
<template>
<div class="flex justify-center pt-2">
<div class="flex w-[1216px] flex-col items-center">
<div class="flex w-full justify-between py-2 items-center">
<button id="create_new_acct_btn" class="flex rounded-full bg-primary text-[#ffffff] flex justify-center
items-center px-6 py-[10px]" @click="onCreateNewClick">
{{ i18next.t("AcctMgmt.CreateNew") }}
</button>
<SearchBar @on-search-account-button-click="onSearchAccountButtonClick"/>
</div>
<div id="acct_mgmt_data_grid" class="flex w-full overflow-y-auto h-[570px]" @scroll="handleScroll">
<DataTable :value="accountSearchResults" dataKey="username" tableClass="w-full mt-4 text-sm relative table-fixed"
:rowClass="getRowClass"
<div class="flex justify-center pt-2">
<div class="flex w-[1216px] flex-col items-center">
<div class="flex w-full justify-between py-2 items-center">
<button
id="create_new_acct_btn"
class="flex rounded-full bg-primary text-[#ffffff] flex justify-center items-center px-6 py-[10px]"
@click="onCreateNewClick"
>
{{ i18next.t("AcctMgmt.CreateNew") }}
</button>
<SearchBar
@on-search-account-button-click="onSearchAccountButtonClick"
/>
</div>
<div
id="acct_mgmt_data_grid"
class="flex w-full overflow-y-auto h-[570px]"
@scroll="handleScroll"
>
<DataTable
:value="accountSearchResults"
dataKey="username"
tableClass="w-full mt-4 text-sm relative table-fixed"
:rowClass="getRowClass"
>
<Column
field="username"
:header="i18next.t('AcctMgmt.Account')"
bodyClass="font-medium"
sortable
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div
@dblclick="onAcctDoubleClick(slotProps.data.username)"
class="account-cell cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis"
>
<Column field="username" :header="i18next.t('AcctMgmt.Account')" bodyClass="font-medium" sortable>
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div @dblclick="onAcctDoubleClick(slotProps.data.username)" class="account-cell cursor-pointer
whitespace-nowrap overflow-hidden text-ellipsis">
{{ slotProps.data.username }}
</div>
<span id="just_create_badge" class="flex ml-4"
v-if="isOneAccountJustCreate && slotProps.data.username === justCreateUsername">
<img src="@/assets/icon-new.svg" alt="New">
</span>
</div>
</template>
</Column>
<Column field="name" :header="i18next.t('AcctMgmt.FullName')" bodyClass="text-neutral-500" sortable>
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div class="fullname-cell whitespace-nowrap overflow-hidden text-ellipsis">
{{ slotProps.data.name }}
</div>
</div>
</template>
</Column>
<Column field="is_admin" :header="i18next.t('AcctMgmt.AdminRights')" bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable flex w-full justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<img v-if="slotProps.data.is_admin" src="@/assets/radioOn.svg" alt="Radio On" class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, false)"
/>
<img v-else src="@/assets/radioOff.svg" alt="Radio Off" class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, true)"
/>
</div>
<div v-else @mouseenter="handleRowMouseOver(slotProps.data.username)" @mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"></div>
</template>
</Column>
<Column field="is_active" :header="i18next.t('AcctMgmt.AccountActivation')" bodyClass="text-neutral-500"
headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable w-full" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<div class="w-full flex justify-center">
<img v-if="slotProps.data.is_active" src="@/assets/radioOn.svg" alt="Radio On" class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, false)"/>
<img v-else src="@/assets/radioOff.svg" alt="Radio Off" class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, true)"/>
</div>
</div>
<div v-else @mouseenter="handleRowMouseOver(slotProps.data.username)" @mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"></div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Detail')" bodyClass="text-neutral-500" headerClass="header-center">
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)">
<img :src="slotProps.data.isDetailHovered ? iconDetailOn : iconDetailOff" alt="Detail" class="btn-detail cursor-pointer" @click="onDetailBtnClick(slotProps.data.username)"
@mouseover="handleDetailMouseOver(slotProps.data.username)"
@mouseout="handleDetailMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Edit')" bodyClass="text-neutral-500" headerClass="header-center">
<template #body="slotProps">
<div class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)">
<img :src="slotProps.data.isEditHovered ? iconEditOn : iconEditOff" alt="Edit" class="btn-edit cursor-pointer"
@click="onEditButtonClick(slotProps.data.username)"
@mouseover="handleEditMouseOver(slotProps.data.username)"
@mouseout="handleEditMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column :header="i18next.t('AcctMgmt.Delete')" bodyClass="text-neutral-500 flex justify-center" headerClass="header-center">
<template #body="slotProps">
<div v-if="!slotProps.data.isCurrentLoggedIn" class="row-container flex-w-full-hoverable w-full flex justify-center" @mouseenter="handleRowMouseOver(slotProps.data.username)">
<img :src="slotProps.data.isDeleteHovered ? iconDeleteRed : iconDeleteGray"
:alt="slotProps.data.isDeleteHovered ? 'hovered' : 'not-hovered'"
class="delete-account cursor-pointer flex"
@mouseover="handleDeleteMouseOver(slotProps.data.username)"
@mouseout="handleDeleteMouseOut(slotProps.data.username)"
@click="onDeleteBtnClick(slotProps.data.username)"
>
</div>
<div v-else @mouseenter="handleRowMouseOver(slotProps.data.username)" @mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"></div>
</template>
</Column>
</DataTable>
</div>
</div>
{{ slotProps.data.username }}
</div>
<span
id="just_create_badge"
class="flex ml-4"
v-if="
isOneAccountJustCreate &&
slotProps.data.username === justCreateUsername
"
>
<img src="@/assets/icon-new.svg" alt="New" />
</span>
</div>
</template>
</Column>
<Column
field="name"
:header="i18next.t('AcctMgmt.FullName')"
bodyClass="text-neutral-500"
sortable
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div
class="fullname-cell whitespace-nowrap overflow-hidden text-ellipsis"
>
{{ slotProps.data.name }}
</div>
</div>
</template>
</Column>
<Column
field="is_admin"
:header="i18next.t('AcctMgmt.AdminRights')"
bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable flex w-full justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<img
v-if="slotProps.data.is_admin"
src="@/assets/radioOn.svg"
alt="Radio On"
class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, false)"
/>
<img
v-else
src="@/assets/radioOff.svg"
alt="Radio Off"
class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, true)"
/>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
<Column
field="is_active"
:header="i18next.t('AcctMgmt.AccountActivation')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div class="w-full flex justify-center">
<img
v-if="slotProps.data.is_active"
src="@/assets/radioOn.svg"
alt="Radio On"
class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, false)"
/>
<img
v-else
src="@/assets/radioOff.svg"
alt="Radio Off"
class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, true)"
/>
</div>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Detail')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<img
:src="
slotProps.data.isDetailHovered
? iconDetailOn
: iconDetailOff
"
alt="Detail"
class="btn-detail cursor-pointer"
@click="onDetailBtnClick(slotProps.data.username)"
@mouseover="handleDetailMouseOver(slotProps.data.username)"
@mouseout="handleDetailMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Edit')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
>
<img
:src="slotProps.data.isEditHovered ? iconEditOn : iconEditOff"
alt="Edit"
class="btn-edit cursor-pointer"
@click="onEditButtonClick(slotProps.data.username)"
@mouseover="handleEditMouseOver(slotProps.data.username)"
@mouseout="handleEditMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Delete')"
bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
>
<img
:src="
slotProps.data.isDeleteHovered
? iconDeleteRed
: iconDeleteGray
"
:alt="
slotProps.data.isDeleteHovered ? 'hovered' : 'not-hovered'
"
class="delete-account cursor-pointer flex"
@mouseover="handleDeleteMouseOver(slotProps.data.username)"
@mouseout="handleDeleteMouseOut(slotProps.data.username)"
@click="onDeleteBtnClick(slotProps.data.username)"
/>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
</DataTable>
</div>
</div>
</div>
</template>
<script setup>
@@ -123,27 +247,27 @@
* delete actions, admin toggle, and infinite scroll.
*/
import { ref, computed, onMounted, watch } from 'vue';
import { useLoadingStore } from '@/stores/loading';
import { useModalStore } from '@/stores/modal';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useLoginStore } from '@/stores/login';
import SearchBar from '../../../components/AccountMenu/SearchBar.vue';
import i18next from '@/i18n/i18n.js';
import { useToast } from 'vue-toast-notification';
import { ref, computed, onMounted, watch } from "vue";
import { useLoadingStore } from "@/stores/loading";
import { useModalStore } from "@/stores/modal";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import { useLoginStore } from "@/stores/login";
import SearchBar from "../../../components/AccountMenu/SearchBar.vue";
import i18next from "@/i18n/i18n.js";
import { useToast } from "vue-toast-notification";
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
ONCE_RENDER_NUM_OF_DATA,
} from "@/constants/constants.js";
import iconDeleteGray from '@/assets/icon-delete-gray.svg';
import iconDeleteRed from '@/assets/icon-delete-red.svg';
import iconEditOff from '@/assets/icon-edit-off.svg';
import iconEditOn from '@/assets/icon-edit-on.svg';
import iconDetailOn from '@/assets/icon-detail-on.svg';
import iconDetailOff from '@/assets/icon-detail-card.svg';
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
ONCE_RENDER_NUM_OF_DATA,
} from "@/constants/constants.js";
import iconDeleteGray from "@/assets/icon-delete-gray.svg";
import iconDeleteRed from "@/assets/icon-delete-red.svg";
import iconEditOff from "@/assets/icon-edit-off.svg";
import iconEditOn from "@/assets/icon-edit-on.svg";
import iconDetailOn from "@/assets/icon-detail-on.svg";
import iconDetailOff from "@/assets/icon-detail-card.svg";
const toast = useToast();
const acctMgmtStore = useAcctMgmtStore();
@@ -155,176 +279,187 @@ const infiniteStart = ref(0);
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccountList);
const infiniteAcctData = computed(() => allAccountResponsive.value.slice(0, infiniteStart.value + ONCE_RENDER_NUM_OF_DATA));
const infiniteAcctData = computed(() =>
allAccountResponsive.value.slice(
0,
infiniteStart.value + ONCE_RENDER_NUM_OF_DATA,
),
);
const loginUserData = ref(null);
const isOneAccountJustCreate = computed(() => acctMgmtStore.isOneAccountJustCreate);
const isOneAccountJustCreate = computed(
() => acctMgmtStore.isOneAccountJustCreate,
);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const inputQuery = ref('');
const inputQuery = ref("");
const fetchLoginUserData = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
};
const moveJustCreateUserToFirstRow = () => {
if(infiniteAcctData.value && infiniteAcctData.value.length){
const index = acctMgmtStore.allUserAccountList.findIndex(user => user.username === acctMgmtStore.justCreateUsername);
if (index !== -1) {
const [justCreateUser] = acctMgmtStore.allUserAccountList[index];
infiniteAcctData.value.unshift(justCreateUser);
}
if (infiniteAcctData.value && infiniteAcctData.value.length) {
const index = acctMgmtStore.allUserAccountList.findIndex(
(user) => user.username === acctMgmtStore.justCreateUsername,
);
if (index !== -1) {
const [justCreateUser] = acctMgmtStore.allUserAccountList[index];
infiniteAcctData.value.unshift(justCreateUser);
}
}
};
const accountSearchResults = computed(() => {
if(!inputQuery.value) {
return infiniteAcctData.value;
}
return acctMgmtStore.allUserAccountList.filter (user => user.username.includes(inputQuery.value));
if (!inputQuery.value) {
return infiniteAcctData.value;
}
return acctMgmtStore.allUserAccountList.filter((user) =>
user.username.includes(inputQuery.value),
);
});
const onCreateNewClick = () => {
acctMgmtStore.clearCurrentViewingUser();
modalStore.openModal(MODAL_CREATE_NEW);
acctMgmtStore.clearCurrentViewingUser();
modalStore.openModal(MODAL_CREATE_NEW);
};
const onAcctDoubleClick = (username) => {
acctMgmtStore.setCurrentViewingUser(username);
modalStore.openModal(MODAL_ACCT_INFO);
}
acctMgmtStore.setCurrentViewingUser(username);
modalStore.openModal(MODAL_ACCT_INFO);
};
const handleDeleteMouseOver = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
};
const handleDeleteMouseOut = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleRowMouseOver = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, true);
acctMgmtStore.changeIsRowHoveredByUser(username, true);
};
const handleRowMouseOut = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleEditMouseOver = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, true);
acctMgmtStore.changeIsEditHoveredByUser(username, true);
};
const handleEditMouseOut = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
acctMgmtStore.changeIsEditHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleDetailMouseOver = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
};
const handleDetailMouseOut = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
acctMgmtStore.changeIsDetailHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const onEditButtonClick = userNameToEdit => {
acctMgmtStore.setCurrentViewingUser(userNameToEdit);
modalStore.openModal(MODAL_ACCT_EDIT);
}
const onEditButtonClick = (userNameToEdit) => {
acctMgmtStore.setCurrentViewingUser(userNameToEdit);
modalStore.openModal(MODAL_ACCT_EDIT);
};
const onDeleteBtnClick = (usernameToDelete) => {
acctMgmtStore.setCurrentViewingUser(usernameToDelete);
modalStore.openModal(MODAL_DELETE);
acctMgmtStore.setCurrentViewingUser(usernameToDelete);
modalStore.openModal(MODAL_DELETE);
};
const getRowClass = (curData) => {
return curData?.isRowHovered ? 'bg-[#F1F5F9]' : '';
return curData?.isRowHovered ? "bg-[#F1F5F9]" : "";
};
const onDetailBtnClick = (dataKey) => {
acctMgmtStore.setCurrentViewingUser(dataKey);
modalStore.openModal(MODAL_ACCT_INFO);
acctMgmtStore.setCurrentViewingUser(dataKey);
modalStore.openModal(MODAL_ACCT_INFO);
};
watch(shouldUpdateList, async(newShouldUpdateList) => {
if (newShouldUpdateList) {
await acctMgmtStore.getAllUserAccounts();
moveJustCreateUserToFirstRow();
}
acctMgmtStore.setShouldUpdateList(false);
watch(shouldUpdateList, async (newShouldUpdateList) => {
if (newShouldUpdateList) {
await acctMgmtStore.getAllUserAccounts();
moveJustCreateUserToFirstRow();
}
acctMgmtStore.setShouldUpdateList(false);
});
const onSearchAccountButtonClick = (inputQueryString) => {
inputQuery.value = inputQueryString;
inputQuery.value = inputQueryString;
};
const setIsActiveInput = async(userData, inputIsActiveToSet) => {
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_active: inputIsActiveToSet,
};
await acctMgmtStore.editAccount(userData.username, userDataToReplace);
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
}
const setIsActiveInput = async (userData, inputIsActiveToSet) => {
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_active: inputIsActiveToSet,
};
await acctMgmtStore.editAccount(userData.username, userDataToReplace);
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
};
const onAdminInputClick = async(userData, inputIsAdminOn) => {
const ADMIN_ROLE_NAME = 'admin';
switch(inputIsAdminOn) {
case true:
await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
case false:
await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
default:
break;
}
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_admin: inputIsAdminOn,
};
const onAdminInputClick = async (userData, inputIsAdminOn) => {
const ADMIN_ROLE_NAME = "admin";
switch (inputIsAdminOn) {
case true:
await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
case false:
await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
default:
break;
}
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_admin: inputIsAdminOn,
};
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
}
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
};
/**
* Loads more account data when the user scrolls near the bottom.
* @param {Event} event - The scroll event from the data grid.
*/
const handleScroll = (event) => {
const container = event.target;
const smallValue = 3;
const container = event.target;
const smallValue = 3;
const isOverScrollHeight = container.scrollTop + container.clientHeight >= container.scrollHeight - smallValue;
if(isOverScrollHeight){
fetchMoreDataVue3();
}
const isOverScrollHeight =
container.scrollTop + container.clientHeight >=
container.scrollHeight - smallValue;
if (isOverScrollHeight) {
fetchMoreDataVue3();
}
};
const fetchMoreDataVue3 = () => {
if(infiniteAcctData.value.length < acctMgmtStore.allUserAccountList.length) {
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
}
if (infiniteAcctData.value.length < acctMgmtStore.allUserAccountList.length) {
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
}
};
onMounted(async () => {
loadingStore.setIsLoading(false);
await fetchLoginUserData();
await acctMgmtStore.getAllUserAccounts();
loadingStore.setIsLoading(false);
await fetchLoginUserData();
await acctMgmtStore.getAllUserAccounts();
});
</script>
<style>
/* Center column text so that radio buttons are also centered */
.header-center .p-column-header-content{
justify-content: center;
.header-center .p-column-header-content {
justify-content: center;
}
</style>

View File

@@ -1,184 +1,305 @@
<template>
<div id="modal_account_edit_or_create_new" class="w-[640px] bg-[#FFFFFF] rounded
shadow-lg flex flex-col">
<ModalHeader :headerText="modalTitle"/>
<div
id="modal_account_edit_or_create_new"
class="w-[640px] bg-[#FFFFFF] rounded shadow-lg flex flex-col"
>
<ModalHeader :headerText="modalTitle" />
<main class="flex row min-h-[96px] my-[32px] flex-col px-6">
<div class="input-row w-full flex py-2 h-[40px] mb-4 items-center
justify-between">
<div class="field-label text-sm flex items-center font-medium justify-end">
<span class="align-right-span flex w-[122px] justify-end">
{{ i18next.t("AcctMgmt.Account") }}
</span>
</div>
<div class="input-and-error flex flex-col">
<input id="input_account_field"
class="w-[454px] rounded p-1 border border-[1px] border-[#64748B] flex items-center h-[40px] outline-none"
v-model="inputUserAccount"
:class="{
'text-[#000000]': isAccountUnique,
'text-[#FF3366]': !isAccountUnique,
'border-[#FF3366]': !isAccountUnique,
}"
:readonly="!isEditable" autocomplete="off"
@dblclick="onInputDoubleClick"
/>
<div v-show="!isAccountUnique" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
</div>
<main class="flex row min-h-[96px] my-[32px] flex-col px-6">
<div
class="input-row w-full flex py-2 h-[40px] mb-4 items-center justify-between"
>
<div
class="field-label text-sm flex items-center font-medium justify-end"
>
<span class="align-right-span flex w-[122px] justify-end">
{{ i18next.t("AcctMgmt.Account") }}
</span>
</div>
<div class="input-and-error flex flex-col">
<input
id="input_account_field"
class="w-[454px] rounded p-1 border border-[1px] border-[#64748B] flex items-center h-[40px] outline-none"
v-model="inputUserAccount"
:class="{
'text-[#000000]': isAccountUnique,
'text-[#FF3366]': !isAccountUnique,
'border-[#FF3366]': !isAccountUnique,
}"
:readonly="!isEditable"
autocomplete="off"
@dblclick="onInputDoubleClick"
/>
<div v-show="!isAccountUnique" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
</div>
<div class="input-row w-full flex py-2 h-[40px] mb-4 items-center
justify-between">
<div class="field-label text-sm flex items-center font-medium justify-end">
<span class="align-right-span flex w-[122px] justify-end">
{{ i18next.t("AcctMgmt.FullName") }}
</span>
</div>
<input id="input_name_field"
class="w-[454px] rounded p-1 border border-[1px] border-[#64748B] flex items-center h-[40px] outline-none"
v-model="inputName" :readonly="!isEditable" @dblclick="onInputDoubleClick" @focus="onInputNameFocus"
autocomplete="off"
/>
</div>
<div v-show="!isSSO" class="input-row w-full flex py-2 mb-4 items-center
justify-between">
<div class="field-label text-sm flex flex-col items-start font-medium justify-end"
:class="{
'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW,
}"
> <!-- Layout of Password field changes depending on whether this is the create or edit modal -->
<span class="align-right-span flex h-full w-[122px] justify-end"
:class="{
'pt-[12px]': whichCurrentModal !== MODAL_CREATE_NEW,
'text-top': whichCurrentModal !== MODAL_CREATE_NEW,
'h-[40px]': whichCurrentModal === MODAL_CREATE_NEW,
'items-center': whichCurrentModal === MODAL_CREATE_NEW,
}">
{{ i18next.t("AcctMgmt.Password") }}
</span>
<span v-if="whichCurrentModal === MODAL_CREATE_NEW" class="dummy-cell
flex w-[122px] h-[40px]">
</span>
</div>
<div class="reset-btn-and-input-pwd-and-error flex flex-col"
:class="{'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW,}"
>
<div v-if="whichCurrentModal !== MODAL_CREATE_NEW" class="flex account-edit-section justify-start w-[454px]">
<button class="flex w-[85px] h-[40px] reset-btn rounded-full border-[1px] border-[#666666] cursor-pointer
items-center justify-center hover:text-[#0099FF] hover:border-[#0099FF] mb-[20px]"
@click="onResetPwdButtonClick"
>
{{ i18next.t("AcctMgmt.Reset") }}
</button>
</div>
<div v-if="whichCurrentModal === MODAL_CREATE_NEW || isResetPwdSectionShow"
class="w-[454px] flex flex-col input-and-error-msg">
<div class="input-and-eye flex items-center w-full h-[40px] relative border-[1px] rounded"
:class="{'border-[#FF3366]': !isPwdLengthValid, 'border-[#64748B]': isPwdLengthValid,}"
>
<input id="input_first_pwd" class="outline-none p-1 w-[352px]" :type="isPwdEyeOn ? 'text' : 'password'"
v-model="inputPwd" :readonly="!isEditable" @dblclick="onInputDoubleClick"
autocomplete="off" :class="{'color-[#FF3366]': !isPwdLengthValid, 'color-[#64748B]': isPwdLengthValid,}"/>
<img id='eye_button' v-if="isPwdEyeOn" src='@/assets/icon-eye-open.svg' class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)" @mouseup="togglePwdEyeBtn(false)" alt=""/>
<img v-else src='@/assets/icon-eye-hide.svg' class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)" @mouseup="togglePwdEyeBtn(false)" alt="eye"/>
</div>
<div class="error-msg-section flex justify-start mt-4">
<img v-show="!isPwdLengthValid" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough") }}
</span>
</div>
</div>
</div>
</div>
<div v-show="false" id="confirm_pwd_row" class="input-row w-full grid grid-cols-2 grid-cols-[122px_1fr] gap-x-4
mb-4 flex items-center"> <!-- 2-by-2 grid; the bottom-left cell is a dummy cell with no content -->
<span v-show="false" class="field-label w-[122px] text-sm flex items-center justify-end text-right font-medium mr-4 whitespace-nowrap"
:class="{
'text-[#000000]': isPwdMatched,
'text-[#FF3366]': !isPwdMatched,
}">
{{ i18next.t("AcctMgmt.ConfirmPassword") }}
</span>
<div v-show="false" class="input-and-toggle-btn w-[454px] flex flex-row rounded border border-[1px] relative items-center
h-[40px] mb-4"
:class="{
'border-[#000000]': isPwdMatched,
'border-[#FF3366]': !isPwdMatched,
}">
<input v-show="false" id="input_second_pwd" class="outline-none p-1 w-[352px]" :type="isPwdConfirmEyeOn ? 'text' : 'password'"
v-model="inputConfirmPwd" :readonly="!isEditable" @dblclick="onInputDoubleClick"
:class="{
'text-[#000000]': isPwdMatched,
'text-[#FF3366]': !isPwdMatched,
}" autocomplete="off"/>
<img v-if="isPwdConfirmEyeOn" src='@/assets/icon-eye-open.svg' class="absolute right-[8px] cursor-pointer"
@click="togglePwdConfirmEyeBtn" alt="eye"/>
<img v-else src='@/assets/icon-eye-hide.svg' class="absolute right-[8px] cursor-pointer" @click="togglePwdConfirmEyeBtn" alt="eye"/>
</div>
<div class="dummy-grid h-[24px]"></div> <!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start">
<img v-show="!isPwdMatched" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ isPwdMatched ? "" : i18next.t("AcctMgmt.PwdNotMatch") }}
</span>
</div>
<div class="dummy-grid h-[24px]"></div> <!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start">
<img v-show="!isPwdLengthValid" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough") }}
</span>
</div>
</div>
<div v-if="whichCurrentModal === MODAL_CREATE_NEW" class="checkbox-row w-full grid-cols-[122px_1fr] gap-x-4 flex py-2 h-[40px] my-4 items-center">
<div class="dummy field-label flex items-center w-[122px]">
</div> <!-- A dummy field is also used here to maintain spacing -->
<section id="account_create_checkboxes_section" class="flex flex-col">
<div class="checkbox-and-text flex">
<IconChecked :isChecked="isSetAsAdminChecked" @click="toggleIsAdmin"/>
<span class="flex checkbox-text">
{{ i18next.t("AcctMgmt.SetAsAdmin") }}
</span>
</div>
<div class="checkbox-and-text flex">
<IconChecked :isChecked="isSetActivedChecked" @click="toggleIsActivated"/>
<span class="flex checkbox-text">
{{ i18next.t("AcctMgmt.ActivateNow") }}
</span>
</div>
</section>
</div>
</main>
<footer class="flex row footer justify-end pr-[32px] pb-8">
<button class="cancel-btn rounded-full w-[92px] h-10 px-2.5 py-6 border border-[1px] border-[#64748B]
flex justify-center items-center text-[#4E5969] font-medium
hover:bg-[#64748B] hover:border-[#CBD5E1] hover:text-[#FFFFFF]"
@click="onCancelBtnClick"
</div>
</div>
</div>
<div
class="input-row w-full flex py-2 h-[40px] mb-4 items-center justify-between"
>
<div
class="field-label text-sm flex items-center font-medium justify-end"
>
<span class="align-right-span flex w-[122px] justify-end">
{{ i18next.t("AcctMgmt.FullName") }}
</span>
</div>
<input
id="input_name_field"
class="w-[454px] rounded p-1 border border-[1px] border-[#64748B] flex items-center h-[40px] outline-none"
v-model="inputName"
:readonly="!isEditable"
@dblclick="onInputDoubleClick"
@focus="onInputNameFocus"
autocomplete="off"
/>
</div>
<div
v-show="!isSSO"
class="input-row w-full flex py-2 mb-4 items-center justify-between"
>
<div
class="field-label text-sm flex flex-col items-start font-medium justify-end"
:class="{
'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW,
}"
>
<!-- Layout of Password field changes depending on whether this is the create or edit modal -->
<span
class="align-right-span flex h-full w-[122px] justify-end"
:class="{
'pt-[12px]': whichCurrentModal !== MODAL_CREATE_NEW,
'text-top': whichCurrentModal !== MODAL_CREATE_NEW,
'h-[40px]': whichCurrentModal === MODAL_CREATE_NEW,
'items-center': whichCurrentModal === MODAL_CREATE_NEW,
}"
>
{{ i18next.t("AcctMgmt.Password") }}
</span>
<span
v-if="whichCurrentModal === MODAL_CREATE_NEW"
class="dummy-cell flex w-[122px] h-[40px]"
>
</span>
</div>
<div
class="reset-btn-and-input-pwd-and-error flex flex-col"
:class="{ 'h-[100px]': whichCurrentModal !== MODAL_CREATE_NEW }"
>
<div
v-if="whichCurrentModal !== MODAL_CREATE_NEW"
class="flex account-edit-section justify-start w-[454px]"
>
<button
class="flex w-[85px] h-[40px] reset-btn rounded-full border-[1px] border-[#666666] cursor-pointer items-center justify-center hover:text-[#0099FF] hover:border-[#0099FF] mb-[20px]"
@click="onResetPwdButtonClick"
>
{{ i18next.t("Global.Cancel") }}
{{ i18next.t("AcctMgmt.Reset") }}
</button>
<button class="confirm-btn rounded-full w-[92px] h-10 px-2.5 py-6
flex justify-center items-center text-[#ffffff] font-medium ml-[16px]"
@click="onConfirmBtnClick" :disabled="isConfirmDisabled"
</div>
<div
v-if="
whichCurrentModal === MODAL_CREATE_NEW || isResetPwdSectionShow
"
class="w-[454px] flex flex-col input-and-error-msg"
>
<div
class="input-and-eye flex items-center w-full h-[40px] relative border-[1px] rounded"
:class="{
'border-[#FF3366]': !isPwdLengthValid,
'border-[#64748B]': isPwdLengthValid,
}"
>
<input
id="input_first_pwd"
class="outline-none p-1 w-[352px]"
:type="isPwdEyeOn ? 'text' : 'password'"
v-model="inputPwd"
:readonly="!isEditable"
@dblclick="onInputDoubleClick"
autocomplete="off"
:class="{
'bg-[#0099FF]': !isConfirmDisabled,
'bg-[#E2E8F0]': isConfirmDisabled,
'color-[#FF3366]': !isPwdLengthValid,
'color-[#64748B]': isPwdLengthValid,
}"
>
{{ i18next.t("Global.Confirm") }}
</button>
</footer>
</div>
/>
<img
id="eye_button"
v-if="isPwdEyeOn"
src="@/assets/icon-eye-open.svg"
class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)"
@mouseup="togglePwdEyeBtn(false)"
alt=""
/>
<img
v-else
src="@/assets/icon-eye-hide.svg"
class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)"
@mouseup="togglePwdEyeBtn(false)"
alt="eye"
/>
</div>
<div class="error-msg-section flex justify-start mt-4">
<img
v-show="!isPwdLengthValid"
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{
isPwdLengthValid
? ""
: i18next.t("AcctMgmt.PwdLengthNotEnough")
}}
</span>
</div>
</div>
</div>
</div>
<div
v-show="false"
id="confirm_pwd_row"
class="input-row w-full grid grid-cols-2 grid-cols-[122px_1fr] gap-x-4 mb-4 flex items-center"
>
<!-- 2-by-2 grid; the bottom-left cell is a dummy cell with no content -->
<span
v-show="false"
class="field-label w-[122px] text-sm flex items-center justify-end text-right font-medium mr-4 whitespace-nowrap"
:class="{
'text-[#000000]': isPwdMatched,
'text-[#FF3366]': !isPwdMatched,
}"
>
{{ i18next.t("AcctMgmt.ConfirmPassword") }}
</span>
<div
v-show="false"
class="input-and-toggle-btn w-[454px] flex flex-row rounded border border-[1px] relative items-center h-[40px] mb-4"
:class="{
'border-[#000000]': isPwdMatched,
'border-[#FF3366]': !isPwdMatched,
}"
>
<input
v-show="false"
id="input_second_pwd"
class="outline-none p-1 w-[352px]"
:type="isPwdConfirmEyeOn ? 'text' : 'password'"
v-model="inputConfirmPwd"
:readonly="!isEditable"
@dblclick="onInputDoubleClick"
:class="{
'text-[#000000]': isPwdMatched,
'text-[#FF3366]': !isPwdMatched,
}"
autocomplete="off"
/>
<img
v-if="isPwdConfirmEyeOn"
src="@/assets/icon-eye-open.svg"
class="absolute right-[8px] cursor-pointer"
@click="togglePwdConfirmEyeBtn"
alt="eye"
/>
<img
v-else
src="@/assets/icon-eye-hide.svg"
class="absolute right-[8px] cursor-pointer"
@click="togglePwdConfirmEyeBtn"
alt="eye"
/>
</div>
<div class="dummy-grid h-[24px]"></div>
<!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start">
<img
v-show="!isPwdMatched"
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ isPwdMatched ? "" : i18next.t("AcctMgmt.PwdNotMatch") }}
</span>
</div>
<div class="dummy-grid h-[24px]"></div>
<!-- Use dummy-grid to maintain the height -->
<div class="error-msg-section flex justify-start">
<img
v-show="!isPwdLengthValid"
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{
isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough")
}}
</span>
</div>
</div>
<div
v-if="whichCurrentModal === MODAL_CREATE_NEW"
class="checkbox-row w-full grid-cols-[122px_1fr] gap-x-4 flex py-2 h-[40px] my-4 items-center"
>
<div class="dummy field-label flex items-center w-[122px]"></div>
<!-- A dummy field is also used here to maintain spacing -->
<section id="account_create_checkboxes_section" class="flex flex-col">
<div class="checkbox-and-text flex">
<IconChecked
:isChecked="isSetAsAdminChecked"
@click="toggleIsAdmin"
/>
<span class="flex checkbox-text">
{{ i18next.t("AcctMgmt.SetAsAdmin") }}
</span>
</div>
<div class="checkbox-and-text flex">
<IconChecked
:isChecked="isSetActivedChecked"
@click="toggleIsActivated"
/>
<span class="flex checkbox-text">
{{ i18next.t("AcctMgmt.ActivateNow") }}
</span>
</div>
</section>
</div>
</main>
<footer class="flex row footer justify-end pr-[32px] pb-8">
<button
class="cancel-btn rounded-full w-[92px] h-10 px-2.5 py-6 border border-[1px] border-[#64748B] flex justify-center items-center text-[#4E5969] font-medium hover:bg-[#64748B] hover:border-[#CBD5E1] hover:text-[#FFFFFF]"
@click="onCancelBtnClick"
>
{{ i18next.t("Global.Cancel") }}
</button>
<button
class="confirm-btn rounded-full w-[92px] h-10 px-2.5 py-6 flex justify-center items-center text-[#ffffff] font-medium ml-[16px]"
@click="onConfirmBtnClick"
:disabled="isConfirmDisabled"
:class="{
'bg-[#0099FF]': !isConfirmDisabled,
'bg-[#E2E8F0]': isConfirmDisabled,
}"
>
{{ i18next.t("Global.Confirm") }}
</button>
</footer>
</div>
</template>
<script setup>
@@ -192,15 +313,19 @@
* username, name, password fields and validation.
*/
import { computed, ref, watch } from 'vue';
import { computed, ref, watch } from "vue";
import i18next from "@/i18n/i18n.js";
import { useModalStore } from '@/stores/modal';
import { useRouter } from 'vue-router';
import { useToast } from 'vue-toast-notification';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import { useModalStore } from "@/stores/modal";
import { useRouter } from "vue-router";
import { useToast } from "vue-toast-notification";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import ModalHeader from "./ModalHeader.vue";
import IconChecked from "@/components/icons/IconChecked.vue";
import { MODAL_CREATE_NEW, MODAL_ACCT_EDIT, PWD_VALID_LENGTH } from '@/constants/constants.js';
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
PWD_VALID_LENGTH,
} from "@/constants/constants.js";
const acctMgmtStore = useAcctMgmtStore();
const modalStore = useModalStore();
@@ -223,157 +348,170 @@ const isSSO = computed(() => acctMgmtStore.currentViewingUser.is_sso);
const username = computed(() => acctMgmtStore.currentViewingUser.username);
const name = computed(() => acctMgmtStore.currentViewingUser.name);
const inputUserAccount = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.username);
const inputName = ref(whichCurrentModal.value === MODAL_CREATE_NEW ? '' : currentViewingUser.value.name);
const inputUserAccount = ref(
whichCurrentModal.value === MODAL_CREATE_NEW
? ""
: currentViewingUser.value.username,
);
const inputName = ref(
whichCurrentModal.value === MODAL_CREATE_NEW
? ""
: currentViewingUser.value.name,
);
const inputPwd = ref("");
const isAccountUnique = ref(true);
const isEditable = ref(true);
// Since adding this watch, filling in the password field no longer clears the account or full name fields.
watch(whichCurrentModal, (newVal) => {
if (newVal === MODAL_CREATE_NEW) {
inputUserAccount.value = '';
inputName.value = '';
} else {
inputUserAccount.value = currentViewingUser.value.username;
inputName.value = currentViewingUser.value.name;
}
if (newVal === MODAL_CREATE_NEW) {
inputUserAccount.value = "";
inputName.value = "";
} else {
inputUserAccount.value = currentViewingUser.value.username;
inputName.value = currentViewingUser.value.name;
}
});
const modalTitle = computed(() => {
return modalStore.whichModal === MODAL_CREATE_NEW ? i18next.t('AcctMgmt.CreateNew') : i18next.t('AcctMgmt.AccountEdit');
return modalStore.whichModal === MODAL_CREATE_NEW
? i18next.t("AcctMgmt.CreateNew")
: i18next.t("AcctMgmt.AccountEdit");
});
const togglePwdEyeBtn = (toBeOpen) => {
isPwdEyeOn.value = toBeOpen;
isPwdEyeOn.value = toBeOpen;
};
const validatePwdLength = () => {
isPwdLengthValid.value = !isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH;
}
isPwdLengthValid.value =
!isResetPwdSectionShow.value || inputPwd.value.length >= PWD_VALID_LENGTH;
};
const onInputDoubleClick = () => {
// Enable edit mode
isEditable.value = true;
}
// Enable edit mode
isEditable.value = true;
};
const onConfirmBtnClick = async () => {
// rule for minimum length
validatePwdLength();
if(!isPwdLengthValid.value) {
// rule for minimum length
validatePwdLength();
if (!isPwdLengthValid.value) {
return;
}
// rule for account uniqueness
switch (whichCurrentModal.value) {
case MODAL_CREATE_NEW:
await checkAccountIsUnique();
if (!isAccountUnique.value) {
return;
}
}
await acctMgmtStore.createNewAccount({
username: inputUserAccount.value,
password: inputPwd.value === undefined ? "" : inputPwd.value,
name: inputName.value,
is_admin: isSetAsAdminChecked.value,
is_active: isSetActivedChecked.value,
});
await toast.success(i18next.t("AcctMgmt.MsgAccountAdded"));
await modalStore.closeModal();
acctMgmtStore.setShouldUpdateList(true);
await router.push("/account-admin");
break;
case MODAL_ACCT_EDIT:
await checkAccountIsUnique();
if (!isAccountUnique.value) {
return;
}
// Note that the old username and new username can be different
// Distinguish between cases with and without a password
if (isResetPwdSectionShow.value) {
await acctMgmtStore.editAccount(currentViewingUser.value.username, {
newUsername: inputUserAccount.value,
password: inputPwd.value,
name: inputName.value === undefined ? "" : inputName.value,
is_active: true,
});
} else {
await acctMgmtStore.editAccount(currentViewingUser.value.username, {
newUsername: inputUserAccount.value,
name: inputName.value === undefined ? "" : inputName.value,
is_active: true,
});
}
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
isEditable.value = false;
break;
default:
break;
}
};
// rule for account uniqueness
switch(whichCurrentModal.value) {
case MODAL_CREATE_NEW:
await checkAccountIsUnique();
if(!isAccountUnique.value) {
return;
}
await acctMgmtStore.createNewAccount({
username: inputUserAccount.value,
password: inputPwd.value === undefined ? '' : inputPwd.value,
name: inputName.value,
is_admin: isSetAsAdminChecked.value,
is_active: isSetActivedChecked.value,
});
await toast.success(i18next.t("AcctMgmt.MsgAccountAdded"));
await modalStore.closeModal();
acctMgmtStore.setShouldUpdateList(true);
await router.push('/account-admin');
break;
case MODAL_ACCT_EDIT:
await checkAccountIsUnique();
if(!isAccountUnique.value) {
return;
}
// Note that the old username and new username can be different
// Distinguish between cases with and without a password
if(isResetPwdSectionShow.value) {
await acctMgmtStore.editAccount(
currentViewingUser.value.username, {
newUsername: inputUserAccount.value,
password: inputPwd.value,
name: inputName.value === undefined ? '' : inputName.value,
is_active: true,
});
} else {
await acctMgmtStore.editAccount(
currentViewingUser.value.username, {
newUsername: inputUserAccount.value,
name: inputName.value === undefined ? '' : inputName.value,
is_active: true,
});
}
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
isEditable.value = false;
break;
default:
break;
}
}
const checkAccountIsUnique = async() => {
// If the user has not modified the field, no backend API call is needed
if(inputUserAccount.value === username.value) {
return true;
}
const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(inputUserAccount.value);
isAccountUnique.value = !isAccountAlreadyExistAPISuccess;
return isAccountUnique.value;
const checkAccountIsUnique = async () => {
// If the user has not modified the field, no backend API call is needed
if (inputUserAccount.value === username.value) {
return true;
}
const isAccountAlreadyExistAPISuccess = await acctMgmtStore.getUserDetail(
inputUserAccount.value,
);
isAccountUnique.value = !isAccountAlreadyExistAPISuccess;
return isAccountUnique.value;
};
const toggleIsAdmin = () => {
if(isEditable){
isSetAsAdminChecked.value = !isSetAsAdminChecked.value;
}
}
if (isEditable.value) {
isSetAsAdminChecked.value = !isSetAsAdminChecked.value;
}
};
const toggleIsActivated = () => {
if(isEditable){
isSetActivedChecked.value = !isSetActivedChecked.value;
}
}
if (isEditable.value) {
isSetActivedChecked.value = !isSetActivedChecked.value;
}
};
const onInputNameFocus = () => {
if(isConfirmDisabled.value){
isConfirmDisabled.value = false;
}
}
if (isConfirmDisabled.value) {
isConfirmDisabled.value = false;
}
};
const onResetPwdButtonClick = () => {
isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
// Must clear the password input field
inputPwd.value = '';
}
isResetPwdSectionShow.value = !isResetPwdSectionShow.value;
// Must clear the password input field
inputPwd.value = "";
};
watch(
[inputPwd, inputUserAccount, inputName],
([newPwd, newAccount, newName]) => {
// Enable the confirm button when all fields are non-empty
if(newAccount.length > 0 && newName.length > 0) {
isConfirmDisabled.value = false;
}
if(whichCurrentModal.value !== MODAL_CREATE_NEW) {
if(isResetPwdSectionShow.value && newPwd.length < PWD_VALID_LENGTH) {
isConfirmDisabled.value = true;
}
}else {
if(newPwd.length < PWD_VALID_LENGTH) {
isConfirmDisabled.value = true;
}
}
[inputPwd, inputUserAccount, inputName],
([newPwd, newAccount, newName]) => {
// Enable the confirm button when all fields are non-empty
if (newAccount.length > 0 && newName.length > 0) {
isConfirmDisabled.value = false;
}
if (whichCurrentModal.value !== MODAL_CREATE_NEW) {
if (isResetPwdSectionShow.value && newPwd.length < PWD_VALID_LENGTH) {
isConfirmDisabled.value = true;
}
} else {
if (newPwd.length < PWD_VALID_LENGTH) {
isConfirmDisabled.value = true;
}
}
},
);
function onCancelBtnClick(){
modalStore.closeModal();
function onCancelBtnClick() {
modalStore.closeModal();
}
</script>
<style>
#modal_account_edit {
background-color: #ffffff;
backdrop-filter: opacity(1); /* Prevent child elements from inheriting parent's opacity */
background-color: #ffffff;
backdrop-filter: opacity(
1
); /* Prevent child elements from inheriting parent's opacity */
}
</style>

View File

@@ -1,25 +1,38 @@
<template>
<div id="modal_account_edit" class="w-[600px] h-[536px] bg-[#FFFFFF] rounded
shadow-lg flex flex-col">
<ModalHeader :headerText='i18next.t("AcctMgmt.AccountInformation")'/>
<main class="flex main-part flex-col px-6 mt-6">
<h1 id="acct_info_user_name" class="text-[32px] leading-[64px] font-medium mb-2">{{ name }}</h1>
<div class="status-container">
<Badge displayText="Admin" :isActivated="is_admin"/>
<Badge displayText="Suspended" :isActivated="!is_active"/>
</div>
<div id="account_visit_info" class="border-b border-b-[#CBD5E1] border-b-[1px] pb-4">
Account: <span class="text-[#0099FF]">{{ username }}</span>, total visits <span class="text-[#0099FF]">
{{ visitTime }}
</span> times.
</div>
</main>
<main class="flex main-part flex-col px-6 py-4">
<ul class="leading-[21px] list-disc list-inside">
<li>[2023-01-01 08:30:25] Account created by</li>
</ul>
</main>
</div>
<div
id="modal_account_edit"
class="w-[600px] h-[536px] bg-[#FFFFFF] rounded shadow-lg flex flex-col"
>
<ModalHeader :headerText="i18next.t('AcctMgmt.AccountInformation')" />
<main class="flex main-part flex-col px-6 mt-6">
<h1
id="acct_info_user_name"
class="text-[32px] leading-[64px] font-medium mb-2"
>
{{ name }}
</h1>
<div class="status-container">
<Badge displayText="Admin" :isActivated="is_admin" />
<Badge displayText="Suspended" :isActivated="!is_active" />
</div>
<div
id="account_visit_info"
class="border-b border-b-[#CBD5E1] border-b-[1px] pb-4"
>
Account: <span class="text-[#0099FF]">{{ username }}</span
>, total visits
<span class="text-[#0099FF]">
{{ visitTime }}
</span>
times.
</div>
</main>
<main class="flex main-part flex-col px-6 py-4">
<ul class="leading-[21px] list-disc list-inside">
<li>[2023-01-01 08:30:25] Account created by</li>
</ul>
</main>
</div>
</template>
<script setup>
@@ -34,24 +47,19 @@
* status badges, and visit count.
*/
import { onBeforeMount, computed, ref } from 'vue';
import i18next from '@/i18n/i18n.js';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import ModalHeader from './ModalHeader.vue';
import Badge from '../../components/Badge.vue';
import { onBeforeMount, computed, ref } from "vue";
import i18next from "@/i18n/i18n.js";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import ModalHeader from "./ModalHeader.vue";
import Badge from "../../components/Badge.vue";
const acctMgmtStore = useAcctMgmtStore();
const visitTime = ref(0);
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const {
username,
name,
is_admin,
is_active,
} = currentViewingUser.value;
const { username, name, is_admin, is_active } = currentViewingUser.value;
onBeforeMount(async() => {
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
visitTime.value = currentViewingUser.value.detail.visits;
onBeforeMount(async () => {
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
visitTime.value = currentViewingUser.value.detail.visits;
});
</script>

View File

@@ -1,45 +1,49 @@
<template>
<div id="modal_container" v-if="modalStore.isModalOpen" class="fixed w-screen h-screen bg-gray-800
flex justify-center items-center">
<div class="flex">
<ModalAccountEditCreate v-if="whichModal === MODAL_ACCT_EDIT || whichModal === MODAL_CREATE_NEW "/>
<ModalAccountInfo v-if="whichModal === MODAL_ACCT_INFO"/>
<ModalDeleteAlert v-if="whichModal === MODAL_DELETE" />
</div>
<div
id="modal_container"
v-if="modalStore.isModalOpen"
class="fixed w-screen h-screen bg-gray-800 flex justify-center items-center"
>
<div class="flex">
<ModalAccountEditCreate
v-if="whichModal === MODAL_ACCT_EDIT || whichModal === MODAL_CREATE_NEW"
/>
<ModalAccountInfo v-if="whichModal === MODAL_ACCT_INFO" />
<ModalDeleteAlert v-if="whichModal === MODAL_DELETE" />
</div>
</template>
</div>
</template>
<script setup>
<script setup>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module views/AccountManagement/ModalContainer Container
* that renders the appropriate account management modal
* based on the current modal type.
*/
/**
* @module views/AccountManagement/ModalContainer Container
* that renders the appropriate account management modal
* based on the current modal type.
*/
import { computed } from 'vue';
import { useModalStore } from '@/stores/modal';
import ModalAccountEditCreate from './ModalAccountEditCreate.vue';
import ModalAccountInfo from './ModalAccountInfo.vue';
import ModalDeleteAlert from './ModalDeleteAlert.vue';
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
} from "@/constants/constants.js";
import { computed } from "vue";
import { useModalStore } from "@/stores/modal";
import ModalAccountEditCreate from "./ModalAccountEditCreate.vue";
import ModalAccountInfo from "./ModalAccountInfo.vue";
import ModalDeleteAlert from "./ModalDeleteAlert.vue";
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
} from "@/constants/constants.js";
const modalStore = useModalStore();
const whichModal = computed(() => modalStore.whichModal);
</script>
const modalStore = useModalStore();
const whichModal = computed(() => modalStore.whichModal);
</script>
<style>
#modal_container {
z-index: 9999;
background-color: rgba(254,254,254, 0.8);
z-index: 9999;
background-color: rgba(254, 254, 254, 0.8);
}
</style>

View File

@@ -1,30 +1,49 @@
<template>
<div id="modal_delete_acct_alert" class="flex shadow-lg rounded-lg w-[564px] flex-col bg-[#ffffff]">
<div class="flex decoration-bar bg-[#FF3366] h-2 rounded-t"></div>
<main id="delete_acct_modal_main" class="flex flex-col items-center justify-center">
<img src="@/assets/icon-large-exclamation.svg" alt="!" class="flex mt-[36px]">
<h1 class="flex mt-4 text-xl font-medium">{{ i18next.t("AcctMgmt.DelteQuestion").toUpperCase() }}</h1>
<div class="clauses flex flex-col mt-4 mb-8 px-8">
<h2 class="flex leading-[21px]">{{ i18next.t("The") }}&nbsp;<span class="text-[#FF3366]">
{{ i18next.t("AcctMgmt.DeleteFirstClause").toUpperCase() }}
</span>
</h2>
<h2 class="flex leading-[21px]">{{ i18next.t("AcctMgmt.DeleteSecondClause") }}</h2>
</div>
</main>
<footer class="flex justify-center py-3 border-t-2 border-t-gray-50 gap-4">
<button id="calcel_delete_acct_btn" class="flex justify-center items-center rounded-full cursor-pointer w-[100px] h-[40px]
bg-[#FF3366] text-[#ffffff]" @click="onNoBtnClick">
{{ i18next.t("No") }}
</button>
<button id="sure_to_delete_acct_btn" class="flex justify-center items-center rounded-full cursor-pointer w-[100px] h-[40px]
border-2 border-[#FF3366] text-[#FF3366]"
@click="onDeleteConfirmBtnClick"
>
{{ i18next.t("Yes") }}
</button>
</footer>
</div>
<div
id="modal_delete_acct_alert"
class="flex shadow-lg rounded-lg w-[564px] flex-col bg-[#ffffff]"
>
<div class="flex decoration-bar bg-[#FF3366] h-2 rounded-t"></div>
<main
id="delete_acct_modal_main"
class="flex flex-col items-center justify-center"
>
<img
src="@/assets/icon-large-exclamation.svg"
alt="!"
class="flex mt-[36px]"
/>
<h1 class="flex mt-4 text-xl font-medium">
{{ i18next.t("AcctMgmt.DelteQuestion").toUpperCase() }}
</h1>
<div class="clauses flex flex-col mt-4 mb-8 px-8">
<h2 class="flex leading-[21px]">
{{ i18next.t("The") }}&nbsp;<span class="text-[#FF3366]">
{{ i18next.t("AcctMgmt.DeleteFirstClause").toUpperCase() }}
</span>
</h2>
<h2 class="flex leading-[21px]">
{{ i18next.t("AcctMgmt.DeleteSecondClause") }}
</h2>
</div>
</main>
<footer class="flex justify-center py-3 border-t-2 border-t-gray-50 gap-4">
<button
id="calcel_delete_acct_btn"
class="flex justify-center items-center rounded-full cursor-pointer w-[100px] h-[40px] bg-[#FF3366] text-[#ffffff]"
@click="onNoBtnClick"
>
{{ i18next.t("No") }}
</button>
<button
id="sure_to_delete_acct_btn"
class="flex justify-center items-center rounded-full cursor-pointer w-[100px] h-[40px] border-2 border-[#FF3366] text-[#FF3366]"
@click="onDeleteConfirmBtnClick"
>
{{ i18next.t("Yes") }}
</button>
</footer>
</div>
</template>
<script setup>
@@ -38,11 +57,11 @@
* modal for account deletion with yes/no buttons.
*/
import { useModalStore } from '@/stores/modal';
import { useRouter } from 'vue-router';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import i18next from '@/i18n/i18n.js';
import { useToast } from 'vue-toast-notification';
import { useModalStore } from "@/stores/modal";
import { useRouter } from "vue-router";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import i18next from "@/i18n/i18n.js";
import { useToast } from "vue-toast-notification";
const acctMgmtStore = useAcctMgmtStore();
const modalStore = useModalStore();
@@ -50,17 +69,19 @@ const toast = useToast();
const router = useRouter();
/** Confirms account deletion, shows success toast, and navigates to account admin page. */
const onDeleteConfirmBtnClick = async() => {
if(await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)){
toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess"));
modalStore.closeModal();
acctMgmtStore.setShouldUpdateList(true);
await router.push("/account-admin");
}
const onDeleteConfirmBtnClick = async () => {
if (
await acctMgmtStore.deleteAccount(acctMgmtStore.currentViewingUser.username)
) {
toast.success(i18next.t("AcctMgmt.MsgAccountDeleteSuccess"));
modalStore.closeModal();
acctMgmtStore.setShouldUpdateList(true);
await router.push("/account-admin");
}
};
/** Cancels deletion and closes the modal. */
const onNoBtnClick = () => {
modalStore.closeModal();
modalStore.closeModal();
};
</script>

View File

@@ -1,14 +1,19 @@
<template>
<header class="w-full flex h-[64px] justify-between pr-[22px] pl-[16px] items-center
border-b border-b-[1px] border-[#CBD5E1]
">
<h1 class="flex text-base font-bold"> {{ headerText }}</h1>
<div class="w-8 h-8 bg-transparent hover:bg-[#e9ecef] rounded-full relative flex justify-center items-center">
<img src="@/assets/icon-x.svg" alt="X" class="flex cursor-pointer absolute"
@click="closeModal"
/>
</div>
</header>
<header
class="w-full flex h-[64px] justify-between pr-[22px] pl-[16px] items-center border-b border-b-[1px] border-[#CBD5E1]"
>
<h1 class="flex text-base font-bold">{{ headerText }}</h1>
<div
class="w-8 h-8 bg-transparent hover:bg-[#e9ecef] rounded-full relative flex justify-center items-center"
>
<img
src="@/assets/icon-x.svg"
alt="X"
class="flex cursor-pointer absolute"
@click="closeModal"
/>
</div>
</header>
</template>
<script setup>
@@ -22,13 +27,13 @@
* header with title text and close button.
*/
import { useModalStore } from '@/stores/modal';
import { useModalStore } from "@/stores/modal";
defineProps({
headerText: {
type: String,
required: true,
}
headerText: {
type: String,
required: true,
},
});
const modalStore = useModalStore();

View File

@@ -1,117 +1,187 @@
<template>
<div class="general my-account flex flex-col items-center w-full h-full p-8">
<main class="flex main-part flex-col px-8 mt-6 w-[720px]">
<h1 id="general_acct_info_user_name" class="text-[32px] leading-[64px] font-medium mb-2">
{{ name }}
</h1>
<div class="general-status-container">
<Badge displayText="Admin" :isActivated="is_admin"/>
<Badge displayText="Suspended" :isActivated="!is_active"/>
<div class="general my-account flex flex-col items-center w-full h-full p-8">
<main class="flex main-part flex-col px-8 mt-6 w-[720px]">
<h1
id="general_acct_info_user_name"
class="text-[32px] leading-[64px] font-medium mb-2"
>
{{ name }}
</h1>
<div class="general-status-container">
<Badge displayText="Admin" :isActivated="is_admin" />
<Badge displayText="Suspended" :isActivated="!is_active" />
</div>
<div
id="general_account_visit_info"
class="w-full border-b border-b-[#CBD5E1] border-b-[1px] mt-3 pb-4 w-full"
>
Total visits
<span class="text-[#0099FF]">
{{ visitTime }}
</span>
times.
</div>
</main>
<main class="flex flex-col pt-6 px-8 w-[720px]">
<h1 class="text-[20px] font-medium mb-4">
{{ i18next.t("AcctMgmt.UserInfo") }}
</h1>
<div class="row w-full flex py-2 h-[40px] mb-4 items-center">
<div class="field-label text-sm flex items-center font-medium mr-4">
<span class="align-right-span flex w-[80px]">
{{ i18next.t("AcctMgmt.Account") }}
</span>
</div>
<div class="account flex flex-col">
<span class="flex text-[#0F172A] text-[16px]">{{ username }}</span>
<div v-show="false" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
<div id="general_account_visit_info" class="w-full border-b border-b-[#CBD5E1] border-b-[1px] mt-3 pb-4
w-full">
Total visits <span class="text-[#0099FF]">
{{ visitTime }}
</span> times.
</div>
</div>
</div>
<div class="row w-full flex py-2 h-[40px] mb-4 items-center text-[14px]">
<div class="field-label text-sm flex font-medium mr-4 box-border">
<span class="flex w-[80px]">
{{ i18next.t("AcctMgmt.FullName") }}
</span>
</div>
<input
v-if="isNameEditable"
id="input_name_field"
class="w-[280px] h-[40px] rounded p-1 border border-[1px] border-[#64748B] flex items-center outline-none"
v-model="inputName"
autocomplete="off"
/>
<div v-else class="not-editable displayable name flex w-[280px]">
{{ name }}
</div>
<div
v-if="isNameEditable"
class="save-btn-container flex ml-[112px]"
@click="onSaveNameClick"
>
<ButtonFilled :buttonText="i18next.t('Global.Save')" />
</div>
<div
v-if="isNameEditable"
class="cancel-btn btn-container flex ml-[8px]"
@click="onCancelNameClick"
>
<Button :buttonText="i18next.t('Global.Cancel')" />
</div>
<div
v-else="isPwdEditable"
class="edit-btn btn-container flex ml-[204px]"
@click="onEditNameClick"
>
<Button :buttonText="i18next.t('Global.Edit')" />
</div>
</div>
<div
class="row w-full flex py-2 mb-4 border-b border-b-[#CBD5E1] border-b-[1px]"
>
<div class="field-label flex flex-col text-sm font-medium mr-4">
<span class="flex h-[40px] w-[80px] items-center">
{{ i18next.t("AcctMgmt.Password") }}
</span>
<div class="flex dummy-cell"></div>
</div>
<div class="flex input-and-error-msg flex-col w-[280px]">
<div
v-if="isPwdEditable"
class="input-and-eye flex items-center h-[40px] relative border-[1px] rounded"
:class="{
'border-[#FF3366]': !isPwdLengthValid,
'border-[#64748B]': isPwdLengthValid,
}"
>
<input
class="outline-none p-1 w-[280px]"
:type="isPwdEyeOn ? 'text' : 'password'"
v-model="inputPwd"
autocomplete="off"
:class="{
'color-[#FF3366]': !isPwdLengthValid,
'color-[#64748B]': isPwdLengthValid,
}"
/>
<img
id="eye_button"
v-if="isPwdEyeOn"
src="@/assets/icon-eye-open.svg"
class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)"
@mouseup="togglePwdEyeBtn(false)"
alt=""
/>
<img
v-else
src="@/assets/icon-eye-hide.svg"
class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)"
@mouseup="togglePwdEyeBtn(false)"
alt="eye"
/>
</div>
<div class="error-msg-section flex justify-start mt-4">
<img
v-show="!isPwdLengthValid"
src="@/assets/icon-alert.svg"
alt="!"
class="exclamation-img flex mr-2"
/>
<span
class="error-msg-text flex text-[#FF3366] h-[24px] text-[14px]"
>
{{
isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough")
}}
</span>
</div>
</div>
<div class="button-and-dummy-column flex flex-col text-[14px]">
<div class="button-group flex w-min-[80px] h-[40px]">
<div
v-if="isPwdEditable"
class="save-btn-container flex ml-[112px]"
@click="onSavePwdClick"
>
<ButtonFilled :buttonText="i18next.t('Global.Save')" />
</div>
</main>
<main class="flex flex-col pt-6 px-8 w-[720px]">
<h1 class="text-[20px] font-medium mb-4">
{{ i18next.t("AcctMgmt.UserInfo") }}
</h1>
<div class="row w-full flex py-2 h-[40px] mb-4 items-center">
<div class="field-label text-sm flex items-center font-medium mr-4">
<span class="align-right-span flex w-[80px]">
{{ i18next.t("AcctMgmt.Account") }}
</span>
</div>
<div class="account flex flex-col">
<span class="flex text-[#0F172A] text-[16px]">{{ username }}</span>
<div v-show="false" class="error-wrapper my-2">
<div class="error-msg-section flex justify-start">
<img src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px]">
{{ i18next.t("AcctMgmt.AccountNotUnique") }}
</span>
</div>
</div>
</div>
<div
v-if="isPwdEditable"
class="cancel-btn btn-container flex ml-[8px]"
@click="onCancelPwdClick"
>
<Button :buttonText="i18next.t('Global.Cancel')" />
</div>
<div class="row w-full flex py-2 h-[40px] mb-4 items-center text-[14px]">
<div class="field-label text-sm flex font-medium mr-4 box-border">
<span class="flex w-[80px]">
{{ i18next.t("AcctMgmt.FullName") }}
</span>
</div>
<input v-if='isNameEditable' id="input_name_field"
class="w-[280px] h-[40px] rounded p-1 border border-[1px] border-[#64748B] flex items-center outline-none"
v-model="inputName"
autocomplete="off"
/>
<div v-else class="not-editable displayable name flex w-[280px]">
{{ name }}
</div>
<div v-if='isNameEditable' class="save-btn-container flex ml-[112px]" @click="onSaveNameClick">
<ButtonFilled :buttonText='i18next.t("Global.Save")' />
</div>
<div v-if='isNameEditable' class="cancel-btn btn-container flex ml-[8px]" @click="onCancelNameClick" >
<Button :buttonText='i18next.t("Global.Cancel")'/>
</div>
<div v-else='isPwdEditable' class="edit-btn btn-container flex ml-[204px]" @click="onEditNameClick" >
<Button :buttonText='i18next.t("Global.Edit")'/>
</div>
<div
v-else
class="reset-btn btn-container flex ml-[460px]"
@click="onResetPwdClick"
>
<Button :buttonText="i18next.t('Global.Reset')" />
</div>
<div class="row w-full flex py-2 mb-4 border-b border-b-[#CBD5E1] border-b-[1px]">
<div class="field-label flex flex-col text-sm font-medium mr-4">
<span class="flex h-[40px] w-[80px] items-center">
{{ i18next.t("AcctMgmt.Password") }}
</span>
<div class="flex dummy-cell"></div>
</div>
<div class="flex input-and-error-msg flex-col w-[280px]">
<div v-if='isPwdEditable' class="input-and-eye flex items-center h-[40px] relative border-[1px] rounded"
:class="{'border-[#FF3366]': !isPwdLengthValid, 'border-[#64748B]': isPwdLengthValid,}"
>
<input class="outline-none p-1 w-[280px]" :type="isPwdEyeOn ? 'text' : 'password'"
v-model="inputPwd"
autocomplete="off" :class="{'color-[#FF3366]': !isPwdLengthValid,
'color-[#64748B]': isPwdLengthValid,}"/>
<img id='eye_button' v-if="isPwdEyeOn" src='@/assets/icon-eye-open.svg' class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)" @mouseup="togglePwdEyeBtn(false)" alt=""/>
<img v-else src='@/assets/icon-eye-hide.svg' class="absolute right-[8px] cursor-pointer"
@mousedown="togglePwdEyeBtn(true)" @mouseup="togglePwdEyeBtn(false)" alt="eye"/>
</div>
<div class="error-msg-section flex justify-start mt-4">
<img v-show="!isPwdLengthValid" src="@/assets/icon-alert.svg" alt="!" class="exclamation-img flex mr-2">
<span class="error-msg-text flex text-[#FF3366] h-[24px] text-[14px]">
{{ isPwdLengthValid ? "" : i18next.t("AcctMgmt.PwdLengthNotEnough") }}
</span>
</div>
</div>
<div class="button-and-dummy-column flex flex-col text-[14px]">
<div class="button-group flex w-min-[80px] h-[40px]">
<div v-if='isPwdEditable' class="save-btn-container flex ml-[112px]"
@click="onSavePwdClick">
<ButtonFilled :buttonText='i18next.t("Global.Save")' />
</div>
<div v-if='isPwdEditable' class="cancel-btn btn-container flex ml-[8px]" @click="onCancelPwdClick" >
<Button :buttonText='i18next.t("Global.Cancel")'/>
</div>
<div v-else class="reset-btn btn-container flex ml-[460px]" @click="onResetPwdClick" >
<Button :buttonText='i18next.t("Global.Reset")'/>
</div>
</div>
<div class="flex dummy-cell h-[40px]"></div>
</div>
</div>
</main>
<main class="session flex flex-col pt-6 px-8 w-[720px]">
<h1 class="text-[20px] font-medium mb-4">
{{ i18next.t("AcctMgmt.Session") }}
</h1>
</main>
</div>
</div>
<div class="flex dummy-cell h-[40px]"></div>
</div>
</div>
</main>
<main class="session flex flex-col pt-6 px-8 w-[720px]">
<h1 class="text-[20px] font-medium mb-4">
{{ i18next.t("AcctMgmt.Session") }}
</h1>
</main>
</div>
</template>
<script setup>
@@ -125,16 +195,16 @@
* viewing and editing the current user's name and password.
*/
import { onMounted, computed, ref } from 'vue';
import i18next from '@/i18n/i18n.js';
import { useLoginStore } from '@/stores/login';
import { useAcctMgmtStore } from '@/stores/acctMgmt';
import Badge from '../../components/Badge.vue';
import { useLoadingStore } from '@/stores/loading';
import Button from '@/components/Button.vue';
import ButtonFilled from '@/components/ButtonFilled.vue';
import { useToast } from 'vue-toast-notification';
import { PWD_VALID_LENGTH } from '@/constants/constants.js';
import { onMounted, computed, ref } from "vue";
import i18next from "@/i18n/i18n.js";
import { useLoginStore } from "@/stores/login";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import Badge from "../../components/Badge.vue";
import { useLoadingStore } from "@/stores/loading";
import Button from "@/components/Button.vue";
import ButtonFilled from "@/components/ButtonFilled.vue";
import { useToast } from "vue-toast-notification";
import { PWD_VALID_LENGTH } from "@/constants/constants.js";
const loadingStore = useLoadingStore();
const loginStore = useLoginStore();
@@ -144,14 +214,10 @@ const toast = useToast();
const visitTime = ref(0);
const currentViewingUser = computed(() => acctMgmtStore.currentViewingUser);
const name = computed(() => currentViewingUser.value.name);
const {
username,
is_admin,
is_active,
} = currentViewingUser.value;
const { username, is_admin, is_active } = currentViewingUser.value;
const inputName = ref(name.value);
const inputPwd = ref('');
const inputPwd = ref("");
const isNameEditable = ref(false);
const isPwdEditable = ref(false);
const isPwdEyeOn = ref(false);
@@ -159,53 +225,53 @@ const isPwdLengthValid = ref(true);
/** Enables the name editing input. */
const onEditNameClick = () => {
isNameEditable.value = true;
isNameEditable.value = true;
};
/** Enables the password editing input. */
const onResetPwdClick = () => {
isPwdEditable.value = true;
isPwdEditable.value = true;
};
/** Validates that the password meets the minimum length requirement. */
const validatePwdLength = () => {
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
isPwdLengthValid.value = inputPwd.value.length >= PWD_VALID_LENGTH;
};
/** Saves the edited name to the server and refreshes user data. */
const onSaveNameClick = async() => {
if(inputName.value.length > 0) {
await acctMgmtStore.editAccountName(username, inputName.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
await acctMgmtStore.getUserDetail(username);
isNameEditable.value = false;
inputName.value = name.value;
}
const onSaveNameClick = async () => {
if (inputName.value.length > 0) {
await acctMgmtStore.editAccountName(username, inputName.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
await acctMgmtStore.getUserDetail(username);
isNameEditable.value = false;
inputName.value = name.value;
}
};
/** Validates and saves the new password. */
const onSavePwdClick = async() => {
validatePwdLength();
if (isPwdLengthValid.value) {
isPwdEditable.value = false;
await acctMgmtStore.editAccountPwd(username, inputPwd.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
inputPwd.value = '';
await acctMgmtStore.getUserDetail(loginStore.userData.username);
}
const onSavePwdClick = async () => {
validatePwdLength();
if (isPwdLengthValid.value) {
isPwdEditable.value = false;
await acctMgmtStore.editAccountPwd(username, inputPwd.value);
await toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
inputPwd.value = "";
await acctMgmtStore.getUserDetail(loginStore.userData.username);
}
};
/** Cancels name editing and restores the original value. */
const onCancelNameClick = () => {
isNameEditable.value = false;
inputName.value = name.value;
isNameEditable.value = false;
inputName.value = name.value;
};
/** Cancels password editing and clears the input. */
const onCancelPwdClick = () => {
isPwdEditable.value = false;
inputPwd.value = '';
isPwdLengthValid.value = true;
isPwdEditable.value = false;
inputPwd.value = "";
isPwdLengthValid.value = true;
};
/**
@@ -213,11 +279,11 @@ const onCancelPwdClick = () => {
* @param {boolean} toBeOpen - Whether to show the password.
*/
const togglePwdEyeBtn = (toBeOpen) => {
isPwdEyeOn.value = toBeOpen;
isPwdEyeOn.value = toBeOpen;
};
onMounted(async() => {
loadingStore.setIsLoading(false);
await acctMgmtStore.getUserDetail(loginStore.userData.username);
onMounted(async () => {
loadingStore.setIsLoading(false);
await acctMgmtStore.getUserDetail(loginStore.userData.username);
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,47 @@
<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':''">
<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}" v-tooltip="tooltip.sidebarView">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
<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 }"
v-tooltip="tooltip.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}" v-tooltip="tooltip.sidebarFilter">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
<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 }"
v-tooltip="tooltip.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}" v-tooltip="tooltip.sidebarTraces">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
<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 }"
v-tooltip="tooltip.sidebarTraces"
>
<span
class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']"
>
rebase
</span>
</li>
@@ -29,13 +54,22 @@
</div>
<!-- Sidebar: State -->
<div id='sidebar_state' class="bg-transparent py-4 w-14 h-screen-main z-10 bottom-0 right-0 absolute">
<div
id="sidebar_state"
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" v-tooltip.left="tooltip.sidebarState">
<span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5"
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
<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"
v-tooltip.left="tooltip.sidebarState"
>
<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>
@@ -43,13 +77,35 @@
</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="tracesViewRef"></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="sidebarFilterRefComp"></SidebarFilter>
<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="tracesViewRef"
></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="sidebarFilterRefComp"
></SidebarFilter>
</template>
<script>
@@ -64,19 +120,19 @@
* statistics.
*/
import { useConformanceStore } from '@/stores/conformance';
import { useConformanceStore } from "@/stores/conformance";
export default {
async beforeRouteEnter(to, from, next) {
const isCheckPage = to.name.includes('Check');
const isCheckPage = to.name.includes("Check");
if (isCheckPage) {
const conformanceStore = useConformanceStore();
switch (to.params.type) {
case 'log':
case "log":
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
break;
case 'filter':
case "filter":
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
break;
}
@@ -84,32 +140,32 @@ export default {
to.meta.file = conformanceStore.routeFile;
}
next();
}
}
},
};
</script>
<script setup>
import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useLoadingStore } from '@/stores/loading';
import { useAllMapDataStore } from '@/stores/allMapData';
import cytoscapeMap from '@/module/cytoscapeMap.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { useMapPathStore } from '@/stores/mapPathStore';
import emitter from '@/utils/emitter';
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';
import ImgCapsule1 from '@/assets/capsule1.svg';
import ImgCapsule2 from '@/assets/capsule2.svg';
import ImgCapsule3 from '@/assets/capsule3.svg';
import ImgCapsule4 from '@/assets/capsule4.svg';
import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import cytoscapeMap from "@/module/cytoscapeMap.js";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
import { useMapPathStore } from "@/stores/mapPathStore";
import emitter from "@/utils/emitter";
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";
import ImgCapsule1 from "@/assets/capsule1.svg";
import ImgCapsule2 from "@/assets/capsule2.svg";
import ImgCapsule3 from "@/assets/capsule3.svg";
import ImgCapsule4 from "@/assets/capsule4.svg";
const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4];
const props = defineProps(['type', 'checkType', 'checkId', 'checkFileId']);
const props = defineProps(["type", "checkType", "checkId", "checkFileId"]);
const route = useRoute();
@@ -117,10 +173,28 @@ const route = useRoute();
const loadingStore = useLoadingStore();
const allMapDataStore = useAllMapDataStore();
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,
postRuleData
const {
processMap,
bpmn,
stats,
insights,
traceId,
traces,
baseTraces,
baseTraceId,
filterTasks,
filterStartToEnd,
filterEndToStart,
filterTimeframe,
filterTrace,
temporaryData,
isRuleData,
ruleData,
logId,
baseLogId,
createFilterId,
cases,
postRuleData,
} = storeToRefs(allMapDataStore);
const cytoscapeStore = useCytoscapeStore();
@@ -129,14 +203,14 @@ const mapPathStore = useMapPathStore();
const numberBeforeMapInRoute = computed(() => {
const path = route.path;
const segments = path.split('/');
const mapIndex = segments.findIndex(segment => segment.includes('map'));
const segments = path.split("/");
const mapIndex = segments.findIndex((segment) => segment.includes("map"));
if (mapIndex > 0) {
const previousSegment = segments[mapIndex - 1];
const match = previousSegment.match(/\d+/);
return match ? match[0] : 'No number found';
return match ? match[0] : "No number found";
}
return 'No map segment found';
return "No map segment found";
});
onBeforeMount(() => {
@@ -157,11 +231,11 @@ const bpmnData = ref({
edges: [],
});
const cytoscapeGraph = ref(null);
const curveStyle = ref('unbundled-bezier');
const mapType = ref('processMap');
const dataLayerType = ref('freq');
const dataLayerOption = ref('total');
const rank = ref('LR');
const curveStyle = ref("unbundled-bezier");
const mapType = ref("processMap");
const dataLayerType = ref("freq");
const dataLayerOption = ref("total");
const rank = ref("LR");
const localTraceId = ref(1);
const sidebarView = ref(false);
const sidebarState = ref(false);
@@ -173,50 +247,54 @@ const sidebarFilterRefComp = ref(null);
const tooltip = {
sidebarView: {
value: 'Visualization Setting',
class: 'ml-1',
value: "Visualization Setting",
class: "ml-1",
pt: {
text: 'text-[10px] p-1'
}
text: "text-[10px] p-1",
},
},
sidebarTraces: {
value: 'Trace',
class: 'ml-1',
value: "Trace",
class: "ml-1",
pt: {
text: 'text-[10px] p-1'
}
text: "text-[10px] p-1",
},
},
sidebarFilter: {
value: 'Filter',
class: 'ml-1',
value: "Filter",
class: "ml-1",
pt: {
text: 'text-[10px] p-1'
}
text: "text-[10px] p-1",
},
},
sidebarState: {
value: 'Summary',
class: 'ml-1',
value: "Summary",
class: "ml-1",
pt: {
text: 'text-[10px] p-1'
}
text: "text-[10px] p-1",
},
},
};
// Computed
const sidebarLeftValue = computed(() => {
return sidebarView.value === true || sidebarTraces.value === true || sidebarFilter.value === true;
return (
sidebarView.value === true ||
sidebarTraces.value === true ||
sidebarFilter.value === true
);
});
// Watch
watch(sidebarView, (newValue) => {
if(newValue) {
if (newValue) {
sidebarFilter.value = false;
sidebarTraces.value = false;
}
});
watch(sidebarFilter, (newValue) => {
if(newValue) {
if (newValue) {
sidebarView.value = false;
sidebarState.value = false;
sidebarTraces.value = false;
@@ -225,7 +303,7 @@ watch(sidebarFilter, (newValue) => {
});
watch(sidebarTraces, (newValue) => {
if(newValue) {
if (newValue) {
sidebarView.value = false;
sidebarState.value = false;
sidebarFilter.value = false;
@@ -234,7 +312,7 @@ watch(sidebarTraces, (newValue) => {
});
watch(sidebarState, (newValue) => {
if(newValue) {
if (newValue) {
sidebarFilter.value = false;
sidebarTraces.value = false;
}
@@ -273,7 +351,7 @@ async function switchRank(rankValue) {
* @param {string} type - 'freq' or 'duration'.
* @param {string} option - The data option (e.g., 'total', 'average').
*/
async function switchDataLayerType(type, option){
async function switchDataLayerType(type, option) {
dataLayerType.value = type;
dataLayerOption.value = option;
createCy(mapType.value);
@@ -284,7 +362,7 @@ async function switchDataLayerType(type, option){
* @param {object} e - Object containing the trace id.
*/
async function switchTraceId(e) {
if(e.id == traceId.value) return;
if (e.id == traceId.value) return;
isLoading.value = true;
traceId.value = e.id;
await allMapDataStore.getTraceDetail();
@@ -299,21 +377,21 @@ async function switchTraceId(e) {
function setNodesData(mapData) {
const mapTypeVal = mapType.value;
const logFreq = {
"total": "",
"rel_freq": "",
"average": "",
"median": "",
"max": "",
"min": "",
"cases": ""
total: "",
rel_freq: "",
average: "",
median: "",
max: "",
min: "",
cases: "",
};
const logDuration = {
"total": "",
"rel_duration": "",
"average": "",
"median": "",
"max": "",
"min": "",
total: "",
rel_duration: "",
average: "",
median: "",
max: "",
min: "",
};
const gateway = {
parallel: "+",
@@ -322,63 +400,63 @@ function setNodesData(mapData) {
};
mapData.nodes = [];
const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
mapSource.vertices.forEach(node => {
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.vertices.forEach((node) => {
switch (node.type) {
case 'gateway':
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,
}
})
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;
case 'event':
if(node.event_type === 'start') mapData.startId = node.id;
else if(node.event_type === 'end') mapData.endId = node.id;
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,
data: {
id: node.id,
type: node.type,
label: node.event_type,
height: 48,
width: 48,
backgroundColor:'#FFFFFF',
bordercolor:'#0F172A',
textColor: '#FF3366',
shape:"ellipse",
freq:logFreq,
duration:logDuration,
}
backgroundColor: "#FFFFFF",
bordercolor: "#0F172A",
textColor: "#FF3366",
shape: "ellipse",
freq: logFreq,
duration: logDuration,
},
});
break;
default:
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:node.label,
height: 48,
width: 216,
textColor: '#0F172A',
backgroundColor:'rgba(0, 0, 0, 0)',
borderradius: 999,
shape:"round-rectangle",
freq:node.freq,
duration:node.duration,
backgroundOpacity: 0,
borderOpacity: 0,
}
})
data: {
id: node.id,
type: node.type,
label: node.label,
height: 48,
width: 216,
textColor: "#0F172A",
backgroundColor: "rgba(0, 0, 0, 0)",
borderradius: 999,
shape: "round-rectangle",
freq: node.freq,
duration: node.duration,
backgroundOpacity: 0,
borderOpacity: 0,
},
});
break;
}
});
@@ -391,26 +469,26 @@ function setNodesData(mapData) {
function setEdgesData(mapData) {
const mapTypeVal = mapType.value;
const logDuration = {
"total": "",
"rel_duration": "",
"average": "",
"median": "",
"max": "",
"min": "",
"cases": ""
total: "",
rel_duration: "",
average: "",
median: "",
max: "",
min: "",
cases: "",
};
mapData.edges = [];
const mapSource = mapTypeVal === 'processMap' ? processMap.value : bpmn.value;
mapSource.edges.forEach(edge => {
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.edges.forEach((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,
source: edge.tail,
target: edge.head,
freq: edge.freq,
duration: edge.duration === null ? logDuration : edge.duration,
style: "dotted",
lineWidth: 1,
},
});
});
@@ -421,20 +499,32 @@ function setEdgesData(mapData) {
* @param {string} type - 'processMap' or 'bpmn'.
*/
async function createCy(type) {
const graphId = document.getElementById('cy');
const mapData = type === 'processMap'? processMapData.value: bpmnData.value;
const mapSource = type === 'processMap' ? processMap.value : bpmn.value;
const graphId = document.getElementById("cy");
const mapData = type === "processMap" ? processMapData.value : bpmnData.value;
const mapSource = type === "processMap" ? processMap.value : bpmn.value;
if(mapSource.vertices.length !== 0){
if (mapSource.vertices.length !== 0) {
setNodesData(mapData);
setEdgesData(mapData);
setActivityBgImage(mapData);
cytoscapeGraph.value = await cytoscapeMap(mapData, dataLayerType.value, dataLayerOption.value, curveStyle.value, rank.value, graphId);
const processOrBPMN = mapType.value === 'processMap' ? 'process' : 'bpmn';
const curveType = curveStyle.value === 'taxi' ? 'elbow' : 'curved';
const directionType = rank.value === 'LR' ? 'horizontal' : 'vertical';
await mapPathStore.setCytoscape(cytoscapeGraph.value, processOrBPMN, curveType, directionType);
};
cytoscapeGraph.value = await cytoscapeMap(
mapData,
dataLayerType.value,
dataLayerOption.value,
curveStyle.value,
rank.value,
graphId,
);
const processOrBPMN = mapType.value === "processMap" ? "process" : "bpmn";
const curveType = curveStyle.value === "taxi" ? "elbow" : "curved";
const directionType = rank.value === "LR" ? "horizontal" : "vertical";
await mapPathStore.setCytoscape(
cytoscapeGraph.value,
processOrBPMN,
curveType,
directionType,
);
}
}
/**
@@ -446,19 +536,30 @@ function setActivityBgImage(mapData) {
const groupSize = Math.floor(nodes.length / ImgCapsules.length);
let nodeOptionArr = [];
const leveledGroups = [];
const activityNodeArray = nodes.filter(node => node.data.type === 'activity');
activityNodeArray.forEach(node => nodeOptionArr.push(node.data[dataLayerType.value][dataLayerOption.value]));
const activityNodeArray = nodes.filter(
(node) => node.data.type === "activity",
);
activityNodeArray.forEach((node) =>
nodeOptionArr.push(node.data[dataLayerType.value][dataLayerOption.value]),
);
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
for(let i = 0; i < ImgCapsules.length; i++) {
for (let i = 0; i < ImgCapsules.length; i++) {
const startIdx = i * groupSize;
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
const endIdx =
i === ImgCapsules.length - 1
? activityNodeArray.length
: startIdx + groupSize;
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
}
for(let level = 0; level < leveledGroups.length; level++) {
leveledGroups[level].forEach(option => {
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[dataLayerType.value][dataLayerOption.value] === option);
curNodes.forEach(curNode => {
curNode.data = {
for (let level = 0; level < leveledGroups.length; level++) {
leveledGroups[level].forEach((option) => {
const curNodes = activityNodeArray.filter(
(activityNode) =>
activityNode.data[dataLayerType.value][dataLayerOption.value] ===
option,
);
curNodes.forEach((curNode) => {
curNode.data = {
...curNode.data,
nodeImageUrl: ImgCapsules[level],
level,
@@ -473,12 +574,12 @@ function setActivityBgImage(mapData) {
try {
const routeParams = route.params;
const file = route.meta.file;
const isCheckPage = route.name.includes('Check');
const isCheckPage = route.name.includes("Check");
isLoading.value = true;
switch (routeParams.type) {
case 'log':
if(!isCheckPage) {
case "log":
if (!isCheckPage) {
logId.value = routeParams.fileId;
baseLogId.value = routeParams.fileId;
} else {
@@ -486,15 +587,17 @@ function setActivityBgImage(mapData) {
baseLogId.value = file.parent.id;
}
break;
case 'filter':
if(!isCheckPage) {
case "filter":
if (!isCheckPage) {
createFilterId.value = routeParams.fileId;
} else {
createFilterId.value = file.parent.id;
}
await allMapDataStore.fetchFunnel(createFilterId.value);
isRuleData.value = Array.from(temporaryData.value);
ruleData.value = isRuleData.value.map(e => sidebarFilterRefComp.value.setRule(e));
ruleData.value = isRuleData.value.map((e) =>
sidebarFilterRefComp.value.setRule(e),
);
break;
}
await allMapDataStore.getAllMapData();
@@ -506,20 +609,20 @@ function setActivityBgImage(mapData) {
await allMapDataStore.getFilterParams();
await allMapDataStore.getTraceDetail();
emitter.on('saveModal', boolean => {
emitter.on("saveModal", (boolean) => {
sidebarView.value = boolean;
sidebarFilter.value = boolean;
sidebarTraces.value = boolean;
sidebarState.value = boolean;
});
emitter.on('leaveFilter', boolean => {
emitter.on("leaveFilter", (boolean) => {
sidebarView.value = boolean;
sidebarFilter.value = boolean;
sidebarTraces.value = boolean;
sidebarState.value = boolean;
});
} catch (error) {
console.error('Failed to initialize map:', error);
console.error("Failed to initialize map:", error);
} finally {
isLoading.value = false;
}

View File

@@ -1,5 +1,10 @@
<template>
<Chart type="line" :data="primeVueSetDataState" :options="primeVueSetOptionsState" class="h-96" />
<Chart
type="line"
:data="primeVueSetDataState"
:options="primeVueSetOptionsState"
class="h-96"
/>
</template>
<script setup>
@@ -14,66 +19,66 @@
* occurrence data with Chart.js bar charts.
*/
import { ref, onMounted } from 'vue';
import { ref, onMounted } from "vue";
import {
setTimeStringFormatBaseOnTimeDifference,
mapTimestampToAxisTicksByFormat,
} from '@/module/timeLabel.js';
} from "@/module/timeLabel.js";
const knownScaleLineChartOptions = {
x: {
type: 'time',
const knownScaleLineChartOptions = {
x: {
type: "time",
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
}
lineHeight: 2,
},
},
time: {
displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11
minute: 'M/d h:mm', // ex: 1/1 1:11
hour: 'M/d h:mm', // ex: 1/1 1:11
day: 'M/d h', // ex: 1/1 1
month: 'y/M/d', // ex: 1911/1/1
},
displayFormats: {
second: "h:mm:ss", // ex: 1:11:11
minute: "M/d h:mm", // ex: 1/1 1:11
hour: "M/d h:mm", // ex: 1/1 1:11
day: "M/d h", // ex: 1/1 1
month: "y/M/d", // ex: 1911/1/1
},
},
ticks: {
display: true,
maxRotation: 0, // Do not rotate labels (range: 0~50)
color: '#64748b',
source: 'labels', // Dynamically display label count proportionally
display: true,
maxRotation: 0, // Do not rotate labels (range: 0~50)
color: "#64748b",
source: "labels", // Dynamically display label count proportionally
},
border: {
color: '#64748b',
color: "#64748b",
},
grid: {
tickLength: 0, // Prevent grid lines from extending beyond the axis
}
},
y: {
tickLength: 0, // Prevent grid lines from extending beyond the axis
},
},
y: {
beginAtZero: true, // Scale includes 0
title: {
display: true,
color: '#334155',
font: {
display: true,
color: "#334155",
font: {
size: 12,
lineHeight: 2
lineHeight: 2,
},
},
},
ticks:{
color: '#64748b',
padding: 8,
ticks: {
color: "#64748b",
padding: 8,
},
grid: {
color: '#64748b',
color: "#64748b",
},
border: {
display: false, // Hide the extra border line on the left side
display: false, // Hide the extra border line on the left side
},
},
},
};
const props = defineProps({
@@ -93,8 +98,8 @@ const props = defineProps({
const primeVueSetDataState = ref(null);
const primeVueSetOptionsState = ref(null);
const colorPrimary = ref('#0099FF');
const colorSecondary = ref('#FFAA44');
const colorPrimary = ref("#0099FF");
const colorSecondary = ref("#FFAA44");
/**
* Compare page and Performance have this same function.
@@ -103,13 +108,15 @@ const colorSecondary = ref('#FFAA44');
* @param customizeOptions.content
* @param customizeOptions.ticksOfXAxis
*/
const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: {
content,
ticksOfXAxis,
},
}) => {
const getCustomizedScaleOption = (
whichScaleObj,
{ customizeOptions: { content, ticksOfXAxis } },
) => {
let resultScaleObj;
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
resultScaleObj = customizeScaleChartOptionTitleByContent(
whichScaleObj,
content,
);
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
return resultScaleObj;
};
@@ -125,14 +132,14 @@ const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
...scaleObjectToAlter,
x: {
...scaleObjectToAlter.x,
ticks: {
...scaleObjectToAlter.x.ticks,
callback: function(value, index) {
// Customize x-axis time ticks based on different intervals
return ticksOfXAxis[index];
},
ticks: {
...scaleObjectToAlter.x.ticks,
callback: function (value, index) {
// Customize x-axis time ticks based on different intervals
return ticksOfXAxis[index];
},
},
},
};
};
@@ -154,21 +161,21 @@ const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => {
return {
...whichScaleObj,
x: {
...whichScaleObj.x,
title: {
...whichScaleObj.x.title,
text: content.x
}
x: {
...whichScaleObj.x,
title: {
...whichScaleObj.x.title,
text: content.x,
},
y: {
...whichScaleObj.y,
title: {
...whichScaleObj.y.title,
text: content.y
}
}
};
},
y: {
...whichScaleObj.y,
title: {
...whichScaleObj.y.title,
text: content.y,
},
},
};
};
/**
@@ -190,7 +197,7 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
// Consider the dimension of chartData.data
// When handling the Compare page case
if(pageName === "Compare"){
if (pageName === "Compare") {
datasetsPrimary = chartData.data[0].data;
datasetsSecondary = chartData.data[1].data;
@@ -210,9 +217,9 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
tension: 0, // Bezier curve tension
borderColor: colorSecondary,
pointBackgroundColor: colorSecondary,
}
},
];
xData = chartData.data[0].data.map(item => new Date(item.x).getTime());
xData = chartData.data[0].data.map((item) => new Date(item.x).getTime());
} else {
datasets = chartData.data;
datasetsArr = [
@@ -221,23 +228,25 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
data: datasets,
fill: false,
tension: 0, // Bezier curve tension
borderColor: '#0099FF',
}
borderColor: "#0099FF",
},
];
xData = chartData.data.map(item => new Date(item.x).getTime());
xData = chartData.data.map((item) => new Date(item.x).getTime());
}
// Customize X axis ticks due to different differences between min and max of data group
// Compare page and Performance page share the same logic
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX);
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
const customizedScaleOption = getCustomizedScaleOption(
knownScaleLineChartOptions, {
knownScaleLineChartOptions,
{
customizeOptions: {
content, ticksOfXAxis,
}
});
content,
ticksOfXAxis,
},
},
);
primeVueSetData = {
labels: xData,
@@ -251,20 +260,20 @@ const getLineChartPrimeVueSetting = (chartData, content, pageName) => {
top: 16,
left: 8,
right: 8,
}
},
},
plugins: {
legend: false, // Legend
tooltip: {
displayColors: true,
titleFont: {weight: 'normal'},
titleFont: { weight: "normal" },
callbacks: {
label: function(tooltipItem) {
// Get the data
const label = tooltipItem.dataset.label || '';
label: function (tooltipItem) {
// Get the data
const label = tooltipItem.dataset.label || "";
// Build the tooltip label with dataset color indicator
return `${label}: ${tooltipItem.parsed.y}`; // Use Unicode block to represent color
// Build the tooltip label with dataset color indicator
return `${label}: ${tooltipItem.parsed.y}`; // Use Unicode block to represent color
},
},
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More