Compare commits

981 Commits

Author SHA1 Message Date
55986a1965 Add OpenAPI fixture validation and fix fixtures to match API spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 23:58:17 +08:00
56bee336db Remove Cypress and update scripts to use Playwright for E2E testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit f828bd0423)
2026-03-22 18:52:23 +08:00
aa2661b556 Add Playwright E2E tests replacing Cypress with MSW integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:52:18 +08:00
67a723207f Remove old Cypress fixture files now served by MSW handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:30:38 +08:00
3d1de913f8 Migrate Cypress E2E from cy.intercept to MSW service worker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 09:30:22 +08:00
b978071f94 Add conditional MSW browser worker startup for E2E testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 08:24:33 +08:00
3918755b7c Migrate Vitest store tests from vi.mock to MSW request handlers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 07:48:53 +08:00
7e052f0d36 Add MSW infrastructure: handlers, fixtures, request-log, and Vitest setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 07:38:02 +08:00
0ff03ec0ef Suppress expected console.error output in error-path tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:05:04 +08:00
0af0ff39d4 Remove unused cytoscape-spread and split build into manual chunks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:39:05 +08:00
aeb6d207c5 Export store interfaces and fix dotenv code block to resolve TypeDoc warnings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:26:29 +08:00
97748bea60 Fix JSDoc errors: remove unsupported @constant tags and correct @param names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:20:05 +08:00
093eabaea3 Add TypeDoc for API documentation generation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:18:07 +08:00
5020d91277 Rewrite README with project overview, tech stack, and usage guide
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:10:28 +08:00
fe8a1e8a00 Replace hard-coded test passwords with crypto.randomUUID() (S2068)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:51:34 +08:00
30ea7711ce Remove outdated doc directory with stale project structure snapshot
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:40:28 +08:00
ced1ff617a Replace void with expect().toBeDefined() for getter side-effect access (S3735)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:30:45 +08:00
af69b6695c Use defineOptions for beforeRouteEnter, eliminating dual script block (S3863)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:25:00 +08:00
dcb497d64b Use void operator for getter side-effect access in tests (S1481)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:11:19 +08:00
289d4213e2 Refactor MainContainer to use onBeforeRouteUpdate Composition API, eliminating dual script block (S3863)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:10:27 +08:00
bbca475bbe Replace async IIFE with onMounted for page initialization (S7785)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:47:34 +08:00
e9e588385b Replace JSON.parse(JSON.stringify()) with lodash-es cloneDeep for deep cloning Vue reactive data (S7784)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:34:31 +08:00
2374363484 Refactor mapPathStore: extract helpers, use for-of, remove unnecessary await, collapse else-if (S3776, S4138, S4123, S6660)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:38:11 +08:00
67a26ae726 Remove TODO comment from conformanceInput store (S1135)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:38:06 +08:00
fad0b6f50e Assign getter access to variable to avoid bare expression statements (S905)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:38:02 +08:00
cb22ce72c1 Collapse if-only-in-else to else-if (S6660)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:37:57 +08:00
4cd61aed0d Use childNode.remove() instead of parentNode.removeChild() (S7762)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:37:50 +08:00
75adbd9c6b Use Math.min to simplify ternary clamping expressions (S7766)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:37:46 +08:00
731649ed0a Use throw instead of return Promise.reject in async error handlers (S7746)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:37:42 +08:00
a54b4cc7bb Use globalThis instead of window for global object access (S7764)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:37:37 +08:00
81df955845 Reduce cognitive complexity by extracting helper functions (S3776)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:03:44 +08:00
201f94e133 Use String.raw for backslash-escaped CSS selectors in Cypress tests (S7780)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:02:33 +08:00
e522bd0796 Move functions to outer scope for clarity (S7721)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:01:29 +08:00
5942c9ff51 Handle caught exceptions properly in catch blocks (S2486)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 01:00:32 +08:00
804d78837b Flip negated conditions to positive for readability (S7735)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:59:37 +08:00
5a936cad97 Use nullish coalescing operator (S6606) and fix duplicate id (S7930)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:55:19 +08:00
ad53494f26 Use optional chaining instead of repeated access (S6582)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:53:48 +08:00
d4429571d5 Revert "Use structuredClone instead of JSON.parse(JSON.stringify()) (S7784)"
This reverts commit 2b0dadedd4.
2026-03-10 00:50:31 +08:00
f0065db295 Remove conditional that returns the same value on both branches (S3923)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:32:50 +08:00
4e5129ca13 Use Set with .has() instead of Array with .includes() (S7776)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:32:30 +08:00
1df984c567 Use default parameter instead of reassignment (S7760)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:31:57 +08:00
468fa63201 Remove zero fractions from number literals (S7748)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:31:40 +08:00
94e200bded Use codePointAt/fromCodePoint instead of charCodeAt/fromCharCode (S7758)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:31:21 +08:00
1c8ac09184 Compare with undefined directly instead of using typeof (S7741)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:30:58 +08:00
5d143d4cc3 Use globalThis instead of window (S7764)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:30:33 +08:00
ebd198e28d Use String.replaceAll() instead of String.replace() with /g flag (S7781)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:29:28 +08:00
2b0dadedd4 Use structuredClone instead of JSON.parse(JSON.stringify()) (S7784)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:28:30 +08:00
3768c6e5ec Use Number.parseInt/parseFloat/isNaN/isFinite instead of globals (S7773)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:26:59 +08:00
12068281e9 Remove unused imports and variables (S1128, S1481, S1854)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:18:03 +08:00
6d13bc9eb8 Replace deprecated PrimeVue components with their successors (S1874)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:14:01 +08:00
716edb33b7 Initialize result to null and remove redundant await in MoreModal switchCaseData
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:38:13 +08:00
1258829c01 Add null guard for infiniteData.length in MoreModal handleScroll
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:37:34 +08:00
a9a4b89111 Lower z-index from 2^31 overflow to reasonable 1100
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:23:54 +08:00
2b1d60b3d1 Add null guard before spreading fetchData result in MoreModal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:23:25 +08:00
2f0728280c Reset loading and scroll state in fetchData catch blocks to prevent stuck UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:23:05 +08:00
dd5096b10b Add radix parameter to parseInt calls in DurationInput
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:46:20 +08:00
2b5938738e Fix isSafeTagId regex to allow hyphens, matching PerformancePage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:45:53 +08:00
bbcbdf542a Add try-catch-finally to switchCaseData to prevent loading stuck on error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:45:30 +08:00
e155c0114f Add explicit return values in store catch blocks to prevent undefined crashes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:45:02 +08:00
e5b40605ec Add null guards for ref(null) stat data in SidebarStates template and show handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:23:09 +08:00
1f3a198a6d Add null guard before spreading error.response.data.detail in uploadLog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:22:31 +08:00
533b78a37f Use col.field instead of array index as v-for key on Column components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:06:50 +08:00
242c96f978 Add null checks for DOM queries in resizeLeftMask/resizeRightMask
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:06:17 +08:00
68806aa29e Add early return guard for undefined uploadFile before accessing .size
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:05:53 +08:00
d47d03c3be Fix timeLabel regex to match decimal values like "45.5 sec"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:05:27 +08:00
d0da9e875e Remove orphaned emitter.emit calls with no matching listener
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:54:10 +08:00
f04da6e278 Fix memory leaks from Cytoscape instances not returned/destroyed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:53:25 +08:00
9aa60414e7 Remove unnecessary async from 9 functions that have no await
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:44:37 +08:00
a5e0e59e0b Add division by zero guards in setLoopData and setIssueData
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:43:52 +08:00
ddec851276 Add consistent en-US locale to toLocaleString() calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:22:49 +08:00
333685fb99 Initialize maxTotal/minTotal from props via watchers in DurationInput
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:22:02 +08:00
a669c8e98e Return empty array instead of undefined in mapTimestampToAxisTicksByFormat
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:39:43 +08:00
35d5b07b07 Remove unnecessary await on non-Promise values in FunnelFilter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:38:58 +08:00
542b941efb Add missing padStart when clamping to min value in DurationInput
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:48:45 +08:00
674afd769c Replace loose equality (==) with strict equality (===) for ID comparisons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:48:28 +08:00
e5e7b8b7b6 Add division by zero guards in TraceFilter traceList and caseTotalPercent
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:47:47 +08:00
1f603af1a9 Add division by zero guard in SidebarTraces traceList computed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:47:20 +08:00
e52a53615f Remove unnecessary async from closeModal()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:41:46 +08:00
0728c9b6b5 Remove unnecessary await on toast.success() and modalStore.closeModal()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:41:30 +08:00
1c70c9b3af Return false instead of undefined in depthFirstSearchMatchTwoPaths bounds checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:40:54 +08:00
7a1b996877 Await fetchAllFiles() in FilesPage onMounted to fix premature loading reset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:40:27 +08:00
07dd30dc50 Add optional chaining for uploadDetail.value.columns in submit()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:33:34 +08:00
ba360120de Return null instead of undefined on 404 in conformance trace detail methods
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:33:17 +08:00
f69eb6ac15 Remove no-op circular watch on isSubmittedData in ConformanceSidebar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:32:56 +08:00
9641fd1e0e Add null/bounds guards for curButton and curPath in mapPathStore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:25:49 +08:00
52dab387dc Add missing return [] in filterAttrs getter when allFilterAttrs is null
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:25:21 +08:00
e6651f27ba Add null guard for valueData in AttributesFilter computed properties
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:12:46 +08:00
75b3a9c782 Remove unused myPiniaPlugin.ts with debug console.log calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:05:08 +08:00
86e08b7146 Fix .toFixed() string-to-number comparison by wrapping with Number()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:05:01 +08:00
b6499020c6 Add division by zero guards in traceList computed and conformance rate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:04:51 +08:00
d3bba9d1a2 Add emitter.off cleanup in onBeforeUnmount for 10 components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:04:43 +08:00
b023d28b74 Add array bounds check in compareSubmit before accessing drag data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:04:33 +08:00
8747421901 Replace toLocaleLowerCase with toLowerCase for ASCII comparisons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:46 +08:00
7e09b21f2e Add fallback for undefined ratio in TraceFilter chart config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:45 +08:00
2349554a26 Track and clear setTimeout in ConformanceSidebar watch on unmount
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:44 +08:00
957f875377 Destroy Chart.js instance on unmount in TimeframesFilter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:43 +08:00
0f09a723a2 Clean up emitter listeners and destroy Cytoscape instance on unmount
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:41 +08:00
e5ddd8c0bd Remove unnecessary try-catch re-throw in refreshToken
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:23:34 +08:00
904749d1fa Remove duplicate sidebarState assignments in sidebar watchers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:23:33 +08:00
2e914aa853 Fix setTimeout losing this context by using arrow function
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:23:32 +08:00
08d7402918 Add null guards for getElementById and uploadDetail in UploadPage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:23:31 +08:00
3ad898aaab Add null coalescing for facets and attributes access in MoreModal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:17 +08:00
d546cc9619 Add optional chaining for i.loc array access in uploadFailedSecond
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:16 +08:00
b10fcc057e Add null guard for querySelector and fix days using raw input value
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:15 +08:00
b3e5554133 Add optional chaining for response.data.log.id access in getFilterDetail
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:13 +08:00
91c6f5e54c Add optional chaining for response.data.roles access in getUserDetail
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:12 +08:00
4c2a79a514 Add null guards for querySelector results and division by zero in AttributesFilter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:11 +08:00
008339a23d Fix computed ref passed without .value to setCurrentGraphId
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:10:09 +08:00
01385d798d Return null for unknown type in getStateData to prevent empty URL request
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:29 +08:00
5bb89a631d Add early return in yTimeRange when yAmount <= 1 to prevent division by zero
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:29 +08:00
14c112cec5 Add bounds checks in highlightClickedPath to prevent undefined access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:27 +08:00
a6d32672bc Add array length guard in TimeframesFilter timeFrameData computed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:26 +08:00
89dc327044 Add empty data guard in MoreModal columnData computed property
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:25 +08:00
f5ade65939 Fix 12-hour format (hh) to 24-hour format (HH) in PerformancePage charts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:23 +08:00
07b28c628d Add bounds checks and fix 12-hour format in CompareDashboard chart functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:31:22 +08:00
9a3d19f53c Replace Math.trunc integer check with Number.isInteger
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:22 +08:00
57214586a8 Return empty string instead of undefined from getFileName on not-found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:21 +08:00
a7e3a2cdce Remove dead v-show=false error block from MyAccount
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:20 +08:00
b46ab4112a Fix falsy check on newUsername to use explicit null comparison
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:13 +08:00
210364a255 Reset infiniteFinish flag on error in MoreModal fetchData
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:12 +08:00
adbb90eeea Fix division by zero in timeRange when amount is 1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:05 +08:00
8a4b6539b9 Add bounds check in highlightMostFrequentPath before array access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:20:05 +08:00
4f660ff08c Add optional chaining for file.parent access across 5 files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:19:50 +08:00
a8984a5de5 Add optional chaining for file.parent access in ConformancePage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:39 +08:00
7bf38b0d07 Add error handling to async calls in AcctMenu
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:37 +08:00
387993da92 Add response structure validation in auth token refresh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:36 +08:00
b5dfae9835 Fix unsafe property access and stale await in DurationInput, ModalAccountInfo, FilesPage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:35 +08:00
5f597961c6 Fix cytoscapeStore: preserve positions, guard access, remove redundant write
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:27 +08:00
8e6ba876e3 Fix null from match() on empty string in formatNumberWithCommas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:26 +08:00
36613d255b Guard setCurrentViewingUser against undefined from find()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:24 +08:00
1ac3d7cd5b Fix sort() mutating state in getter by copying array first
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:18 +08:00
63fa11c44e Wrap localStorage JSON.parse in try-catch in cytoscapeMap
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:17 +08:00
2a2eeabac7 Fix XSS in uploadFailedSecond default case with escapeHtml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:16 +08:00
b58659295b Fix null from match() crash in formatMaxTwo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:14 +08:00
c6d073e119 Fix getYTicksByIndex truncating integers by using toFixed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:09 +08:00
4dbed9fe56 Fix .trim() on null in conformance cases getter and add fallback returns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:10:08 +08:00
19a39bbbff Clean up dead code, typos, and minor style issues
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:00:31 +08:00
932275e4d4 Remove abandoned confirm-password feature hidden with v-show=false
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:00:26 +08:00
69d31c6c8b Remove dead refs and empty focus handlers from LoginPage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:00:24 +08:00
2710910829 Remove dead imports and constants from mapCompareStore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:00:19 +08:00
6141e70235 Fix missing bounds checks, unsafe JSON.parse, and cleanup issues
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:56:47 +08:00
48784010ad Fix chart data issues: 24-hour format, Y-axis bounds, rounding modes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:55:45 +08:00
881dccc1ab Fix memory leaks from Tippy.js instances and unremoved event listeners
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:54:38 +08:00
9acd722929 Fix splice(-1,1) removing last user when logged-in user not found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:52:08 +08:00
3db725e52c Fix static destructuring from reactive source in MyAccount and ModalAccountInfo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:51:23 +08:00
8f610f1244 Fix array destructuring bug in moveJustCreateUserToFirstRow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:50:18 +08:00
ac4405068f Fix HTML injection risks in FilesPage and UploadPage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:48:23 +08:00
ca75a06612 Update .gitignore 2026-03-09 13:19:16 +08:00
89fae2cda7 Update MainContainer module documentation to match current route-guard responsibilities
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:41:46 +08:00
6e3aaca3b1 Extract reusable auth guard decision logic and test router auth behavior against it
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:39:10 +08:00
28fd83242c Use UTF-8 safe return-to encoding and decoding across router and login
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:35:40 +08:00
28464214bc Remove refreshToken navigation side effects and let callers handle redirects
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:32:06 +08:00
1670054356 Clear all auth cookies when API token refresh fails before redirecting to login
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:29:04 +08:00
b53f58cb0c Clear refresh token cookie during logout to enforce full session termination
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:25:53 +08:00
cf45a43a37 Remove obsolete uncaught-exception policy helper and its unit test
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:16:50 +08:00
955e9ceda9 Move auth-entry checks to router guard and simplify MainContainer route logic
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:14:20 +08:00
f3d11ebbcb Refactor MainContainer child routes to consistent relative-path nesting
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:10:52 +08:00
b3f4ace13f Enforce requiresAuth routes in global router guard with login return-to redirects
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:07:56 +08:00
90cc6689c8 Guard Vite API proxy setup behind VUE_APP_API_URL and reuse it in preview
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:04:13 +08:00
a8cd590a11 Require access token presence in MainContainer auth gate before route entry
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 19:01:03 +08:00
0948a82eb5 Align compare navbar state mapping with MAP and PERFORMANCE tabs
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 18:58:01 +08:00
f567e86898 Remove global Cypress uncaught-exception handler to keep default fail-fast behavior
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 18:46:09 +08:00
7d368bf0c3 Remove per-spec uncaught-exception swallowing in Cypress E2E tests
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 18:32:48 +08:00
7309c97502 Remove redundant self-assignment watcher in navbar state
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 14:14:11 +08:00
2927037bbe Separate lint check and auto-fix scripts to avoid side effects
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 13:55:33 +08:00
2721aed928 Handle invalid return-to payloads without misclassifying login as failed
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 13:52:47 +08:00
d5464ebc2d Restrict Cypress uncaught exception suppression to known benign errors
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 13:49:51 +08:00
fc43ca67ca Rename single-word Vue files to multi-word names and update references
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 13:26:12 +08:00
ae03b9cedd Enable multi-word Vue component names with explicit naming
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 13:01:16 +08:00
d03041c2e3 Enable vue/no-side-effects-in-computed-properties and refactor computed state sync
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:45:39 +08:00
c88646eba3 Enable vue/return-in-computed-property and add explicit fallback returns
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:39:11 +08:00
1bf8355092 Enable vue/no-unused-vars and fix template loop variable
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:34:27 +08:00
2a2948fd24 Re-enable key Vue lint rules and fix resulting violations
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:30:17 +08:00
52a36e3a7c Resolve remaining lint violations and stabilize ESLint config
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:24:45 +08:00
847904c49b Apply repository-wide ESLint auto-fix formatting pass
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:11:57 +08:00
7c48faaa3d Translate Cypress support comments and add command JSDoc
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:57:58 +08:00
ef9cf2de8c Migrate router type augmentation to Vue 3 runtime core
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:55:06 +08:00
28c861ab0e Align Cypress base URL and redirect test port
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:52:34 +08:00
08c1adba37 Fix unit test script to run repository test suite
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:50:00 +08:00
1ad94358e4 Remove unsupported ignore-path option from lint script
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:47:28 +08:00
d7caf08d1f Fix design file metadata mapping in files store
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 10:47:49 +08:00
8acb1b50de Persist relative return-to path for post-login redirect
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 10:44:53 +08:00
e275e79a63 Sanitize Cytoscape tooltip labels to prevent XSS
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 10:41:48 +08:00
1d621bf304 Translate all Chinese comments and strings to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 20:03:19 +08:00
7d5918837b Add try-catch to async IIFEs to prevent unhandled rejections
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:11:27 +08:00
ede7becb3a Remove await on non-promise values
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:06:29 +08:00
1b5f97dc9a Remove unnecessary persist:true from acctMgmt store
The store only holds transient data (user list refetched from API,
hover/menu UI states) that should not survive page reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:58:39 +08:00
07a2518e76 Remove unused $moment and $emitter from globalProperties
These were set for Options API (this.$moment, this.$emitter) but all
components now use <script setup> with direct imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:53:00 +08:00
5b3130ea9c Compute refresh token expiry fresh on each sign-in
The expiry date was computed once at store init time and went stale
in long-running SPA sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:49:14 +08:00
ba7c1c7cd0 Rename allUserAccoutList to allUserAccountList (fix typo)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:43:12 +08:00
fe4738b04c Add encodeURIComponent for username in API URL paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:58:30 +08:00
984eaa96b7 Remove duplicate getUserData call in moveCurrentLoginUserToFirstRow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:25:10 +08:00
9a8b45b151 Deduplicate refreshToken by delegating to auth.js refreshTokenAndGetNew
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 09:21:25 +08:00
cd71815c97 Remove artificial delay from apiError, show toast immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:59:52 +08:00
589783d481 Fix typos in error messages, constants, and UI labels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:55:33 +08:00
3693446d1b Add event listener cleanup to prevent memory leaks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:50:19 +08:00
b2b00c4542 Add optional chaining for querySelector result in Files.vue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:45:27 +08:00
564ead23bf Add null guard for performanceData before accessing time properties
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:45:06 +08:00
ac00183096 Initialize nodePositions structure before nested access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:44:54 +08:00
c602848ad7 Add null check before tip.hide() on mouseout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:44:42 +08:00
5058400d72 Fix filterTaskData computed property referencing itself
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:39:14 +08:00
ac9f958bef Remove meaningless return statements inside forEach callbacks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:27:12 +08:00
d7df6a2615 Fix setTimeChartData called with yMax twice instead of yMax and yMin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:26:58 +08:00
eea79c852b Fix open redirect vulnerability in return-to URL after login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:13:12 +08:00
ddab7b3fe9 Add missing path=/ to setCookieWithoutExpiration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:59:40 +08:00
2a4aa9db00 Add optional chaining to error.response.status in catch blocks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:55:49 +08:00
eeea16be38 Fix getters mutating state on repeated access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:31:59 +08:00
ef8ce0d778 Fix showCancelButton using string instead of boolean in renameModal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:23:45 +08:00
4b7c08e2f9 Fix moment format using month token MM instead of minute mm
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:17:09 +08:00
3f1f8fb680 Fix BaseInfiniteFirstCases getter using wrong case for state property
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:10:26 +08:00
702d508d37 Fix incorrect hour/minute calculation in simpleTimeLabel()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:05:41 +08:00
f4fbae8a5c Extract duplicate API path logic into helper function in allMapData store
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:22:19 +08:00
ec0035a182 Extract duplicate API path logic into helper functions in conformance and files stores
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:16:26 +08:00
c3d0add548 Rename createAccont.cy.js to createAccount.cy.js to fix typo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:09:18 +08:00
d7db387edf Clean up i18n: translate 404 page Chinese text and remove placeholder German locale
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:02:33 +08:00
8d358516b8 Translate remaining Chinese JSDoc and CSS comments to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:53:00 +08:00
d16cc46604 Delete yarn.lock 2026-03-06 20:09:02 +08:00
c782a3b079 Add file header and VITE_CACHE_DIR to .env
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:47:47 +08:00
9c6a51029b Support VITE_CACHE_DIR environment variable for custom cache directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 19:40:18 +08:00
7fec6cb63f Add JSDoc documentation and file headers to all source files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:57:58 +08:00
3b7b6ae859 Migrate all Vue components from Options API to <script setup>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:57:58 +08:00
a619be7881 Convert all store files from JavaScript to TypeScript
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:47:57 +08:00
90048d0505 Standardize store exports to named useXxxStore convention
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:25:00 +08:00
147b16ca34 Add centralized API client with axios interceptors, remove vue-axios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:44:33 +08:00
6af7253d08 Upgrade vue-router from 4 to 5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:17:06 +08:00
1f5673040f Upgrade ESLint from 9 to 10 and add @eslint/js as explicit dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:12:00 +08:00
ab8bbb086b Upgrade @types/node from 20 to 25 and chartjs-plugin-dragdata from 1 to 2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:01:39 +08:00
ec61bcd701 Fix E2E tests for PrimeVue 4 CSS class rename (p-dropdown to p-select)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:47:25 +08:00
8e480ed669 Upgrade Vitest from 3 to 4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:39:17 +08:00
a950f254ad Upgrade Vite from 6 to 7 and @vitejs/plugin-vue from 5 to 6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:35:01 +08:00
4e22643999 Upgrade Tailwind CSS from 3 to 4 with CSS-first config migration
Migrate tailwind.config.js to @theme in CSS, replace PostCSS plugins
with @tailwindcss/postcss, add @reference to 12 Vue scoped styles using
@apply, and remove autoprefixer (now built into Tailwind 4).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:29:03 +08:00
08688793ac Upgrade Pinia from 2 to 3 and pinia-plugin-persistedstate from 3 to 4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:17:13 +08:00
941f1e1dbe Upgrade ESLint from 8 to 9 with flat config migration
Replace .eslintrc.cjs with eslint.config.mjs, upgrade eslint-plugin-vue
to 10, eslint-plugin-cypress to 6, @vue/eslint-config-prettier to 10,
and remove @rushstack/eslint-patch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:12:17 +08:00
de92a723a2 Upgrade Cypress from 13 to 15
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:03:23 +08:00
f958efb38b Upgrade i18next from 23 to 25 and cytoscape-popper from 2 to 4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:53:09 +08:00
c182b297c9 Upgrade Prettier from 2 to 3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:41:27 +08:00
e656b3ce99 Upgrade vue-router, vue-toast-notification, @types/node, and eslint-plugin-cypress
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:36:42 +08:00
b988037968 Remove incorrect @types/vue and @types/vue-router Vue 2 type packages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:36:01 +08:00
bcb318b8f6 Upgrade jsdom from 20 to 28 and eslint-plugin-vue to 9.33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:35:27 +08:00
85225c1e30 Upgrade PrimeVue from 3 to 4 with Aura theme preset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:32:34 +08:00
1d047786af Upgrade Tailwind CSS from 3.2 to 3.4 (4.x deferred due to breaking config changes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:16:41 +08:00
d30b31091f Upgrade Vitest from 0.25 to 3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:13:32 +08:00
eaf2a1d2e7 Upgrade Vite from 4 to 6 and @vitejs/plugin-vue from 4 to 5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:12:53 +08:00
9ea9752440 Upgrade Cypress from 12 to 13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:11:46 +08:00
735df448a2 Upgrade minor and patch dependencies (axios, chart.js, cytoscape, typescript, sass, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:09:12 +08:00
aa86d4a3f9 Upgrade Pinia from 2.0 to 2.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:06:09 +08:00
3521593f41 Upgrade Vue from 3.2 to 3.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:05:39 +08:00
5d6d6310be Remove unused PrimeVue PickList and Button global registrations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:58:46 +08:00
b259f23799 Replace date-fns with chartjs-adapter-moment to eliminate duplicate date libraries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:51:13 +08:00
1d0d938193 Replace let with const where variable is never reassigned in Vue files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:38:40 +08:00
64372c7043 Replace loose equality (== null, != null) with strict equality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:22:36 +08:00
3eec131ae0 Fix typos: createfilterId, sidevarFilterRef, selecteName, namber
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:15:35 +08:00
802a5e51fd Replace let with const where variable is never reassigned
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:57:36 +08:00
04841d84f2 Remove dead code and incorrect unused comments
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:54:51 +08:00
523515459b Remove .ts extension from import paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:50:01 +08:00
dfd5706bcf Replace .map() with .forEach() where return value is unused
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:46:01 +08:00
79811435de Fix typos: updataFilter, updataConformance, setPrevioiusPage, reallyDeldet, tooken, pageAdimin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:43:09 +08:00
6cb08df2e4 Remove npx, i, and npm packages that should not be in dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:36:30 +08:00
12c06e9eee Remove duplicate autoprefixer, postcss, tailwindcss from dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:35:53 +08:00
ee920c0806 Move vue-router from devDependencies to dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:35:20 +08:00
6dbef06124 Remove html-webpack-plugin from devDependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:31:41 +08:00
3763b36e62 Remove webpack-env type reference from tsconfig.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:29:30 +08:00
ffa45e2f07 Fix Vite config: replace Webpack devServer with Vite server, remove invalid envDir and transpileDependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:28:46 +08:00
b07f1274e8 Fix TypeScript errors: missing closing brace in vue.d.ts and undefined NodePosition type
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:19:21 +08:00
bd2b4ce5db Fix CYTOSCAPE_NODE_POSTITION typo to CYTOSCAPE_NODE_POSITION
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:18:54 +08:00
2fb88e8458 Fix Navbar route name typos 'Performanc' to 'Performance'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:18:36 +08:00
c7aa32ef6d Guard baseResponse.data access when baseLogId is falsy in getAllTrace
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:14:07 +08:00
833427b224 Fix deleteFile undefined $toast and reversed delete/fetch order
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:09:37 +08:00
6178f09cfe Fix abbreviateNumber result.trim() return value not assigned back
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:08:43 +08:00
2141f514c5 Fix pageAdmin setPrevioiusPageUsingActivePage writing to wrong property
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:08:00 +08:00
eac33d4888 Add mapPathStore tests with mock Cytoscape objects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:02:13 +08:00
9eb8881c57 Fix module-level store init in files.js, apiError.js, cytoscapeMap.js and add files store tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:59:30 +08:00
5be29ddd51 Add escapeHtml utility and apply to all user-controllable SweetAlert2 html
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:52:26 +08:00
954b41b555 Add Secure and SameSite=Lax flags to all cookie operations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:51:14 +08:00
64832bb5f9 Fix router guard broken cookie check to use isLuciaLoggedIn cookie
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:45:04 +08:00
fba2efe21e Fix MainContainer beforeRouteEnter missing try-catch and next() call
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:43:17 +08:00
2768b5d052 Fix refreshToken() undefined config, wrong axios.defaults, and missing re-throw
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:40:48 +08:00
43283aab95 Fix expired calculation to be 6 months from now instead of setting to June
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:39:52 +08:00
905f546227 Rewrite old E2E tests to use fixture-based API mocking, eliminating need for real credentials
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:31:16 +08:00
dc0a98f819 Remove sensitive data from tracked files before BFG history cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:00:58 +08:00
c91d278f1b Add Files to Discover entry flow E2E tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:27:38 +08:00
6dd182b5e9 Add Discover tab navigation and 404 page E2E tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:56:40 +08:00
584a73b90c Add Discover page E2E tests with real API fixture data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:38:20 +08:00
0ab037dac0 Add edge case tests and SweetAlert2 modal interaction tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:06:35 +08:00
6641bc1f8f Add E2E tests for my-account, account info modal, and compare tab
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:52:38 +08:00
1a4062487e Add E2E tests for logout, account CRUD, and file operations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:40:46 +08:00
733bfd7509 Add Cypress E2E tests with fixture-based API mocking for UI regression protection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:10:04 +08:00
676b70caa0 Add component tests for ModalHeader, IconChecked, and Conformance result components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:45:30 +08:00
fa58e665d5 Add component tests for presentational components and Login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:32:57 +08:00
529e9a4aa1 Add store tests with mocked axios and apiError
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:30:33 +08:00
83c2db7582 Add unit tests for utils and module pure functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:14:13 +08:00
e596bcd18e Update .gitignore 2026-03-05 18:59:28 +08:00
Cindy Chang
5b3c0050b9 fix: dotted solid edge style bug 2024-09-05 17:49:01 +08:00
Cindy Chang
8635c8d3e2 refine last commit 2024-09-05 14:58:26 +08:00
Cindy Chang
322f05de14 separately save vertical and horizontal node positions 2024-09-05 14:55:14 +08:00
Cindy Chang
b467b82474 Merge branch 'main' of github.com:dspim/lucia-frontend 2024-09-03 16:09:59 +08:00
Cindy Chang
11d5109164 basic compare map routing 2024-09-03 15:33:25 +08:00
Cindy Chang
276950a739 Merge branch 'hotfix' 2024-09-03 14:37:47 +08:00
Cindy Chang
364d2a58b4 hotfix: vertical view switching 2024-09-03 14:26:51 +08:00
Cindy Chang
d16f7a264e save button effect 2024-09-03 09:27:46 +08:00
Cindy Chang
35c7ac355e fix vertical path bug by not handling TB case 2024-09-02 13:12:12 +08:00
Cindy Chang
7b7b722efb button with border done 2024-08-30 16:59:03 +08:00
Cindy Chang
725cc76ac2 refine default highlight for all six views 2024-08-30 11:16:03 +08:00
Cindy Chang
700b701984 my account page without component button 2024-08-30 09:41:28 +08:00
Cindy Chang
9cf10f5108 my account cancel button added 2024-08-29 16:04:07 +08:00
Cindy Chang
3a44b74bbc responsive name 2024-08-29 14:41:41 +08:00
Cindy Chang
1883818b8b persist installed. MyAccount in progress 2024-08-29 14:19:27 +08:00
Cindy Chang
495fcf7b03 new glowing capsule images 2024-08-29 11:01:12 +08:00
Cindy Chang
ad568e57ad self-loop control-point-step-size 2024-08-29 10:33:19 +08:00
Cindy Chang
b76c432386 WIP my account page 2024-08-28 15:52:12 +08:00
Cindy Chang
4b56130278 1. highlight last edge. 2. padEnd() to left align capsule label 2024-08-28 14:09:32 +08:00
Cindy Chang
f3b9f7cd41 refactor create paths method and highlight path method 2024-08-28 11:46:54 +08:00
Cindy Chang
5164313082 refine "password" layout for both cases 2024-08-28 09:21:14 +08:00
Cindy Chang
63743280f6 fix first index of mapGraphPathToInsight nested object 2024-08-27 16:32:52 +08:00
Cindy Chang
554411ace1 sometimes highlightMostFrequentPath works 2024-08-27 16:03:01 +08:00
Cindy Chang
3f993741c8 default highlight most freq trace 2024-08-27 14:51:26 +08:00
Cindy Chang
56096a01de WIP My Account. 2024-08-27 14:25:20 +08:00
Cindy Chang
49b8233909 fix BPMN multiple selection bug 2024-08-27 11:14:13 +08:00
Cindy Chang
b72d87fb52 BPMN view: remove radio buttons 2024-08-27 11:05:07 +08:00
Cindy Chang
09237a0759 MyAccount page prototype 2024-08-27 09:32:06 +08:00
Cindy Chang
7243100d9c refine isConfirmDisabled 2024-08-26 13:59:02 +08:00
Cindy Chang
f938eae2fc Merge branch 'acct_mgmt' 2024-08-26 13:54:13 +08:00
Cindy Chang
ff8179404b consider the situation when reset section is not open. consider the situation when password is not sent to backend 2024-08-26 13:53:49 +08:00
Cindy Chang
97e01c93fd new version of password edit. Reset button added 2024-08-26 13:40:33 +08:00
Cindy Chang
247a4dbef8 mousedown mouseup eye button 2024-08-26 11:50:13 +08:00
Cindy Chang
26295d5b55 reset map trace button 2024-08-26 09:42:55 +08:00
Cindy Chang
b5d5b1456d force-directed layout 2024-08-23 11:19:46 +08:00
Cindy Chang
ab15087c2d refine: glow width 2024-08-21 16:38:27 +08:00
Cindy Chang
23a21f1b0a refine nodes 2024-08-21 15:33:55 +08:00
Cindy Chang
85b8536f3a dualonNodeClickHighlightEdges and onEdgeClickHighlightNodes 2024-08-21 15:10:50 +08:00
Cindy Chang
c3c2861a8f highlight node and edge 2024-08-21 14:45:59 +08:00
Cindy Chang
d56ea98780 highlight path 2024-08-21 14:25:34 +08:00
Cindy Chang
4ab0d4765b WIP: depthFirstSearchMatchTwoPaths can have one true 2024-08-21 12:22:33 +08:00
Cindy Chang
fbed897d52 WIP: matchGraphPathWithInsightsPath 2024-08-21 10:00:05 +08:00
Cindy Chang
9b7b382962 WIP: DFS 2024-08-19 17:07:56 +08:00
Cindy Chang
2de9a2b3e6 WIP: createAllPaths 2024-08-19 16:19:12 +08:00
Cindy Chang
27d11997bd insight path blue radio button 2024-08-19 14:29:43 +08:00
Cindy Chang
1191d87f93 WIP: insight path tabs 2024-08-19 13:32:50 +08:00
Cindy Chang
0950214f19 separate cases for text-margin-x 2024-08-19 11:53:14 +08:00
Cindy Chang
aebf25a271 WIP: blue overlay edge for cytoscape 2024-08-19 11:16:56 +08:00
Cindy Chang
975b8340b8 fix: #316 cancel and close hover effect 2024-08-16 16:58:13 +08:00
Cindy Chang
6fcd7efbfc WIP: cytoscape edge text can be highlighted 2024-08-16 16:05:00 +08:00
Cindy Chang
3c6644bc63 WIP: #316 close modal 2024-08-16 14:32:30 +08:00
Cindy Chang
9842bf678f fix: #336 add name at info 2024-08-16 13:40:20 +08:00
Cindy Chang
fe3c44659c fix: #325 autocomplete="off" 2024-08-16 12:45:35 +08:00
Cindy Chang
2342ed1b8a overwrite primevue style 2024-08-16 11:22:55 +08:00
Cindy Chang
83bcf17364 almost all summary are done 2024-08-16 11:10:20 +08:00
Cindy Chang
43cbfb0529 Map filename added 2024-08-16 10:47:17 +08:00
Cindy Chang
772302d079 fix: use filter to replace find 2024-08-15 14:36:59 +08:00
Cindy Chang
305771ce5a WIP: level capsule. However, one capsule is failed 2024-08-15 14:26:08 +08:00
Cindy Chang
9a7442b4ef WIP: map with transparent default bg 2024-08-15 13:46:33 +08:00
Cindy Chang
fb335f996d Map. dagre and spread algorithm 2024-08-15 10:54:04 +08:00
Cindy Chang
ffe97ed63f sonar fix 3 issues and now 0 left 2024-08-14 15:45:10 +08:00
Cindy Chang
b3049dafe0 delete gamunu.vscode-yarn 2024-08-14 15:41:42 +08:00
Cindy Chang
1dbe7e716f fix: #330 mouseout effect for edit and delete button 2024-08-14 11:31:57 +08:00
Cindy Chang
840d81da9f fix: #333 hover blue. fix: #332 v-if replacing v-show 2024-08-14 11:14:35 +08:00
Cindy Chang
2ec7029887 fix: #319 only allow confirm button when all the fields are filled in 2024-08-14 10:44:04 +08:00
Cindy Chang
df341c26ab fix: #329 text ellipsis. Sonar 2024-08-14 09:59:09 +08:00
Cindy Chang
2a188c34ec fix: #332 not sure if vue2 way is really fixing the vue3 way 2024-08-13 16:41:40 +08:00
Cindy Chang
2c0d5fe8ee sonar 1 left. all regex fixed 2024-08-13 13:52:05 +08:00
Cindy Chang
db8f8eefd6 sonar 4 left. cookie regex 2024-08-13 13:37:11 +08:00
Cindy Chang
471d8273c1 sonnar 8 left. random number fixed 2024-08-13 13:24:05 +08:00
Cindy Chang
699d7a4f6b sonar 11 left. super-linear fixed 2024-08-13 11:45:01 +08:00
Cindy Chang
1e0796e56e sonar 12 left. XSS on Performance page 2024-08-13 10:44:11 +08:00
Cindy Chang
58646ff91a sonar scanner 14 left. XSS fixed 2024-08-13 10:40:43 +08:00
Cindy Chang
8c58f53d4f sonar security hotspots 2024-08-12 11:30:08 +08:00
Cindy Chang
d8c1e84622 fix: #327 add needed await keyword 2024-08-09 16:07:08 +08:00
Cindy Chang
17e51ca98a fix #324 add error message for length < 6 2024-08-09 15:26:57 +08:00
Cindy Chang
6204361b43 SonarQube done 2024-08-09 15:01:05 +08:00
Cindy Chang
6c38ada281 sonar 1 left 2024-08-09 14:54:22 +08:00
Cindy Chang
995b09cc02 sonar 2 left 2024-08-09 14:41:30 +08:00
Cindy Chang
972b92640f fix: #331 add hover effect for self (logged in user) 2024-08-09 14:25:57 +08:00
Cindy Chang
5ef681eea8 fix: #313 fix again. Don't know why sonar-qube causes this 2024-08-09 14:16:28 +08:00
Cindy Chang
62cc0eeb52 Merge branch 'main' into sonar 2024-08-09 13:52:13 +08:00
Cindy Chang
3fc9bb9659 sonar 3 left 2024-08-09 13:49:19 +08:00
Cindy Chang
39096126c5 feature and sonar-qube: add missing delete API 2024-08-09 13:38:36 +08:00
Cindy Chang
3f29ce9acb v-if for AcctMenu 2024-08-09 11:35:56 +08:00
Cindy Chang
3037299fd6 mark ids 2024-08-09 11:28:44 +08:00
Cindy Chang
a29f3c6406 fix: #325 move moveCurrentLoginUserToFirstRow to store 2024-08-09 09:45:00 +08:00
Cindy Chang
c37e46cff4 WIP #325 2024-08-08 16:37:03 +08:00
Cindy Chang
b46a6ab995 sonar 9 left; Resolved as much as possible 2024-08-08 11:40:29 +08:00
Cindy Chang
335b52cc70 sonar 10 left 2024-08-08 10:51:07 +08:00
Cindy Chang
e313b92521 sonar 12 left 2024-08-08 10:15:16 +08:00
Cindy Chang
85151884fe sonar 2024-08-08 09:50:41 +08:00
Cindy Chang
8ff8f93b2a sonnar 2024-08-08 09:33:12 +08:00
Cindy Chang
8acd6e4a87 fix: #326 suspended should be !is_active 2024-08-08 09:02:22 +08:00
Cindy Chang
8fa1a7b8b3 #334 fixed v-if=isAdmin 2024-08-07 16:29:15 +08:00
Cindy Chang
caa1b9cc70 Merge branch 'sonar' 2024-08-05 09:32:20 +08:00
Cindy Chang
4ba7961a5a sonar 2024-08-02 13:24:15 +08:00
Cindy Chang
86cfb409c3 sonar 41 left 2024-08-02 11:15:15 +08:00
Cindy Chang
a33eaa3a41 sonar 58 left 2024-08-01 16:06:30 +08:00
Cindy Chang
a426f22db0 sonar medium 65 left 2024-08-01 15:49:03 +08:00
Cindy Chang
405dd7f992 sonar 2024-08-01 15:13:05 +08:00
Cindy Chang
1eda4dfb80 sonar medium 83 left 2024-08-01 13:38:19 +08:00
Cindy Chang
e8e8179c05 sonar 2024-08-01 12:52:31 +08:00
Cindy Chang
719501dc4b sonar medium 2024-08-01 11:35:18 +08:00
Cindy Chang
311c98f57f sonar 2024-07-29 14:47:23 +08:00
Cindy Chang
09e38dc3c4 sonar medium 2024-07-29 11:11:00 +08:00
Cindy Chang
2150a4ac79 sonar medium 2024-07-29 10:58:53 +08:00
Cindy Chang
81315167aa sonar medium 2024-07-29 09:27:25 +08:00
Cindy Chang
c85ee86f08 sonar medium 2024-07-26 11:39:02 +08:00
Cindy Chang
83c87746e9 sonar low all done 2024-07-26 11:18:52 +08:00
Cindy Chang
47da80b424 sonar low 2024-07-26 11:16:05 +08:00
Cindy Chang
a5f271ccc8 sonar low 2024-07-26 10:48:38 +08:00
Cindy Chang
2ca4fdd0c6 sonar low 2024-07-26 10:40:45 +08:00
Cindy Chang
71b58af96d add needed break 2024-07-26 09:51:47 +08:00
Cindy Chang
15805b4dc5 refactor on actRadioData 2024-07-26 09:49:39 +08:00
Cindy Chang
54f0d29c99 refactor ConformanceTimeRrange created 2024-07-22 15:31:46 +08:00
Cindy Chang
5230a3a99b refactor submitConformance 2024-07-22 15:20:31 +08:00
Cindy Chang
28cdf9822d refactor setTaskByCategoryOnRadioEmitting 2024-07-22 14:59:24 +08:00
Cindy Chang
9f48c1d2b0 refactor duration.js 2024-07-22 14:52:52 +08:00
Cindy Chang
486baa6e54 fix: prev refactor 2024-07-22 14:29:13 +08:00
Cindy Chang
3ef1540f8c refactor setChartData - slope calculation 2024-07-22 14:25:38 +08:00
Cindy Chang
e5b15957cc sonar qube 2024-07-22 14:14:57 +08:00
Cindy Chang
3c81356fa2 sonar qube 2024-07-22 13:58:23 +08:00
Cindy Chang
adcdb2bdc0 refactor NavBar.vue 2024-07-22 13:52:48 +08:00
Cindy Chang
258a25972a reduce cognitive complexity at ConformanceSidebar.vue 2024-07-22 13:36:19 +08:00
Cindy Chang
5407a4e2aa sonar 2024-07-22 13:20:19 +08:00
Cindy Chang
56c94f1d80 sonarqube 2024-07-22 11:16:16 +08:00
Cindy Chang
07d6e75a6a sonar qube; ternary if 2024-07-16 15:42:30 +08:00
Cindy Chang
6a689e419c sonar qube; getCookie refactored 2024-07-16 15:18:31 +08:00
Cindy Chang
fd5b65a3b6 sonar qube 2024-07-16 13:46:30 +08:00
Cindy Chang
d2e92b367f sonar qube - 'If' statement should not be the only statement in 'else' block 2024-07-16 13:20:04 +08:00
Cindy Chang
3f5f710336 WIP: sonar qube 2024-07-16 09:25:34 +08:00
Cindy Chang
1836ef5fbf fix: sonar qube 2024-07-16 09:16:15 +08:00
Cindy Chang
2517fb07af fix: sonar qube - Add an "alt" attribute to this image. 2024-07-16 09:11:13 +08:00
Cindy Chang
f6c130f46d fix: sonar qube - Add an "alt" attribute to this image. 2024-07-16 09:07:27 +08:00
Cindy Chang
d0a433754a fix: sonar qube - Multiline support is limited to browsers supporting ES5 only. 2024-07-16 09:05:30 +08:00
Cindy Chang
01555d71b0 fix: #318 enable confirm button when newPwd or newConfirmPwd is changed 2024-07-15 15:44:40 +08:00
Cindy Chang
cab484ac00 fix: acct_mgmt_button 2024-07-15 15:39:59 +08:00
Cindy Chang
4c9633a395 Revert "fix: #316 modal closing too slow bug"
This reverts commit 6b7778fe9f.
2024-07-10 11:06:07 +08:00
Cindy Chang
6b7778fe9f fixed: modal closing too slow bug 2024-07-10 10:58:38 +08:00
Cindy Chang
bba7f24672 fix: #319 add watch of inputConfirmPwd inside onMounted 2024-07-10 10:53:48 +08:00
Cindy Chang
b70f241ab5 save 2024-07-10 10:39:15 +08:00
Cindy
0d9bfaa40d refine close modal time 2024-07-10 10:26:42 +08:00
Cindy
423499b77c fixed: #319 put watch of all input fieds function inside "onMounted" function 2024-07-10 10:18:30 +08:00
Cindy
58cfd3f31f test add local develop environment 2024-07-10 09:45:15 +08:00
Cindy Chang
74bc22b736 fixed: #313 use index [ ] instead of splice function to prevent removal 2024-07-09 15:08:55 +08:00
Cindy Chang
a2703bef6e app.$http = axios; Can LOGIN now 2024-07-09 14:53:21 +08:00
Cindy Chang
8b99a230d7 TypeScript bugs fixed 2024-07-09 13:53:20 +08:00
Cindy Chang
5c46fd6ce7 WIP: TypeScript bugs 2024-07-09 13:46:15 +08:00
Cindy Chang
af5ab081de 1. npm install --save-dev @types/axios @types/vue @types/vue-router
2. create file vue-router.d.ts
2. pinia use my plugin
3. npm i --save-dev @types/cytoscape. npm i --save-dev @types/cytoscape-dagre. npm i --save-dev @types/cytoscape-popper.
4. add apiError.d.ts npm install --save-dev @types/vue-router.
5. add vue-axios.d.ts
2024-07-09 12:00:34 +08:00
Cindy Chang
ca4d6d0127 change account-admin url 2024-07-04 15:21:07 +08:00
Cindy Chang
19f7ae5f38 delete console 2024-07-04 11:15:32 +08:00
Cindy Chang
fc37d5d37d delete console 2024-07-04 11:09:41 +08:00
Cindy Chang
d8d70f01f3 fix: #310 keep loading animation bug is related to cytoscape undefined bug. And cytoscape node position remembering feature is not actually finished correctly yet before. Now remembering feature is done. 2024-07-04 11:07:25 +08:00
Cindy Chang
8a77df285b feature: keypress of search bar 2024-07-03 15:42:09 +08:00
Cindy Chang
7f1efba2b3 fix: need to intentionally click sort button when Cypress editAccount. Change "visible" to "exist". 2024-07-03 15:28:13 +08:00
Cindy Chang
880cac23a4 Cypress: edit account and see success message 2024-07-03 14:57:46 +08:00
Cindy Chang
6e39f8be99 Cypress: account duplication check 2024-07-03 14:19:07 +08:00
Cindy Chang
ba6450d4a0 Cypress: confirm password message 2024-07-03 14:06:05 +08:00
Cindy Chang
405cc6ea82 Cypress: delete an account 000000 2024-07-03 13:55:49 +08:00
Cindy Chang
b6fa0b375c acctMgmt.ts by ChatGPT 2024-07-03 13:15:38 +08:00
Cindy Chang
1991b4df63 feature: TypeScript 2024-07-03 12:08:16 +08:00
Cindy Chang
ba4f70213d fix: file name of cypress file 2024-07-03 11:41:51 +08:00
Cindy Chang
18959f213b automation test: create account positive case. 2024-07-02 12:16:23 +08:00
Cindy Chang
c33f387c82 refine confirm disable status of account page 2024-07-02 11:09:31 +08:00
Cindy Chang
da557fdd32 fix: use push instead of wrong ... operator 2024-07-01 10:41:53 +08:00
Cindy Chang
24ccdd47ae feature: remember node positions after refreshing pages 2024-07-01 10:39:53 +08:00
Cindy Chang
69a3f27cb2 fix: #306. At MainContainer, use cookie to determine instead of using pinia state 2024-07-01 08:52:38 +08:00
Cindy Chang
2a8826c962 hide account 2024-06-28 16:45:19 +08:00
Cindy Chang
2110388a2d feature: cytoscape node positions are remembered 2024-06-28 16:20:41 +08:00
Cindy Chang
b890df9de6 Merge branch 'acct_mgmt' of github.com:dspim/lucia-frontend into acct_mgmt 2024-06-28 13:54:02 +08:00
Cindy Chang
8054bf89cd fix: #178. refresh token. if not logged in then refresh token; else redirect to login page. 2024-06-28 13:53:47 +08:00
Cindy Chang
a4aab21b98 feature: refresh token. if not logged in then refresh token; else redirect to login page. 2024-06-28 12:10:19 +08:00
Cindy Chang
9b2bab9124 feature: admin role edit API 2024-06-27 14:21:27 +08:00
Cindy Chang
65fdb2a945 fix: #258 YYYY/MM/DD 2024-06-27 13:48:57 +08:00
Cindy Chang
4418930d41 fix: #258. format precision YYYYMMDD 2024-06-27 13:45:16 +08:00
Cindy Chang
3fe427fcc3 update single account pinia state 2024-06-27 11:39:44 +08:00
Cindy Chang
21fb6f616a feature: set is active API 2024-06-27 10:36:19 +08:00
Cindy Chang
b12d026f0e editable status control 2024-06-27 08:58:40 +08:00
Cindy Chang
b2c084657d edit account and logout feature 2024-06-26 15:35:51 +08:00
Cindy Chang
14783654a5 feature: search bar works. Beautiful two return code. 2024-06-26 12:40:49 +08:00
Cindy Chang
965aaeb097 feature: just create badge. Important thing is the await keyword 2024-06-26 11:46:53 +08:00
Cindy Chang
353eecad81 refine infinite scroll - infiniteStart is a ref variable and infiniteAcctData is a computed one. 2024-06-25 16:28:36 +08:00
Cindy Chang
877c6acfb1 fix: #216. setHighlightedNavItemOnLanding 2024-06-25 15:00:11 +08:00
Cindy Chang
5d5730fc29 visit API, is_admin badge API, is_active badge API, 2024-06-25 12:53:50 +08:00
Cindy Chang
6d11849dae duplicate account check 2024-06-25 12:08:53 +08:00
Cindy Chang
38b268e8c5 Merge branch 'main' into acct_mgmt 2024-06-25 09:04:22 +08:00
Cindy Chang
963645f071 fix: performance page linechart tooltip time display with time unit 2024-06-25 09:03:18 +08:00
Cindy Chang
00d086ff1d delete account and later on modal closing effect. 2024-06-24 10:09:39 +08:00
Cindy Chang
b55cc0a6d6 vue3 infinite scroll 2024-06-24 09:07:11 +08:00
Cindy Chang
9b0d54bf5e feature: create POST api done. 2024-06-21 15:21:24 +08:00
Cindy Chang
0fb0d8b529 WIP: delete alert modal prototype. 2024-06-21 14:20:52 +08:00
Cindy Chang
ad8c555632 feature: acct list can hover pencil btn 2024-06-21 13:17:24 +08:00
Cindy Chang
1b8c8629a9 WIP: can hover icon-delete. can have self on the first row on list.
API get all users done
2024-06-21 11:14:49 +08:00
Cindy Chang
193bc315ca WIP: create feature checkboxes layout 2024-06-20 16:54:37 +08:00
Cindy Chang
05caf819bb WIP: validate confirm password and show error message 2024-06-20 15:54:14 +08:00
Cindy Chang
26441d3979 WIP: account info modal prototype with a little bit of pinia state mgmt. 2024-06-20 12:13:39 +08:00
Cindy Chang
d6a79687da WIP: account edit modal layout done (now: without password rows) 2024-06-20 10:56:03 +08:00
Cindy Chang
b7862ab164 WIP account mgmt data-table prototype and modal prototype. 2024-06-19 15:55:02 +08:00
Cindy Chang
50c98892c4 fix: #304. Compare page, shift digit of percentage value 2024-06-19 13:36:16 +08:00
Cindy Chang
69835ad05b WIP: acct mgmt search bar and data grid header. 2024-06-19 11:35:16 +08:00
Cindy Chang
96ee2eb7fb fix: #303 of cycle eff tooltip. Change background chart grid lines. 2024-06-19 11:00:03 +08:00
Cindy Chang
b10e135fee fix: make legend disappear by not declaring plugins twice 2024-06-19 10:21:15 +08:00
Cindy Chang
80529b85fe WIP: acct mgmt search bar 2024-06-19 10:12:16 +08:00
Cindy Chang
341fd61d07 WIP: account management menu can be toggled by head button now. 2024-06-18 16:38:02 +08:00
Cindy Chang
5d33b6481c fix: #299 Cycle efficiency X axis should only display to YYYYMM. 2024-06-18 10:47:58 +08:00
Cindy Chang
adb2ab29af refine use ticks[ticks.length - 1].value 2024-06-18 09:52:43 +08:00
Cindy Chang
6c9322d1bc fix: #258 2024-06-18 09:25:13 +08:00
Cindy Chang
94dbe1dee6 WIP: #258 Compare page 2024-06-18 09:12:18 +08:00
Cindy Chang
9b88de4c3f fix: #302. Shouldn't use mod and shouldn't double divide. 2024-06-17 23:03:46 +08:00
Cindy Chang
cee61dbd93 fix: #287 Compare page 2024-06-17 22:46:32 +08:00
Cindy Chang
7750f913a8 ticks.length solves the Y axis bug. 2024-06-17 22:32:57 +08:00
Cindy Chang
541c0080aa import moment and declare primevue option 2024-06-17 21:51:19 +08:00
Cindy Chang
9f121eb5d4 WIP: explicit declare primevue option of one of the chart and the bug is somewhat solved 2024-06-17 21:31:41 +08:00
Cindy Chang
3ce17fc436 clean console 2024-06-17 20:54:00 +08:00
Cindy Chang
4c18dfd8d6 shouldn't use mod(%) operator.
shouldn't divide hour and then divide day again.
should use substring.
should split into 8 instead of 6.
2024-06-17 20:53:49 +08:00
Cindy Chang
452c9358c9 should not be ms but s 2024-06-17 11:34:13 +08:00
Cindy Chang
03806dad3c fix: #290 fileInput.value = '' 2024-06-17 11:12:28 +08:00
Cindy Chang
1665e51301 fix #296 "DASHBOARD" --> "PERFORMANCE" 2024-06-17 09:36:45 +08:00
Cindy Chang
505aebebb0 fix #292 remove "i" icon 2024-06-17 09:34:12 +08:00
Cindy Chang
3edac09881 fix #289 locale 2024-06-17 09:26:06 +08:00
Cindy Chang
d0d6c482ee fix #216 with cookie mgmt. 2024-06-14 16:44:17 +08:00
Cindy Chang
5bf98a1b74 WIP #216. The timing is not good. After user password is confirmed, the isLoggedIn boolean is still false, and thus causing the website to stay at the login page 2024-06-14 16:17:20 +08:00
Cindy Chang
8d34b80b5c fix #299 by setting y.reverse 2024-06-14 15:01:12 +08:00
Cindy Chang
8c09e8ae40 fix #258 reopen data_with_log_小兒(日) 2024-06-14 14:00:29 +08:00
Cindy Chang
d47b17f575 fix #301 should push to "/files" 2024-06-14 11:30:43 +08:00
Cindy Chang
13bc3882ed fix #300 handle compare page by handling data[0], data[1] 2024-06-14 10:57:31 +08:00
Cindy Chang
0689e79013 WIP #300 Performance page done. Use ref() instead of reactive() in independant Vue3 component. 2024-06-13 15:40:51 +08:00
Cindy Chang
f6989c4759 WIP same as the previoius commit. Tried to extract out an independent vue component to prevent shared primevue option.
X axis is with bug now.
2024-06-13 13:35:58 +08:00
Cindy Chang
310c416fd7 WIP #300 Tried to extract out an independent vue component to prevent shared primevue option 2024-06-13 09:48:19 +08:00
Cindy Chang
c016e2aa41 WIP about deep cloning and Y axis and object sharing bug 2024-06-12 16:06:17 +08:00
Cindy Chang
339657879a fix #287 by using simple division 2024-06-12 12:27:51 +08:00
Cindy Chang
235504a3fb fix #293. stay at MAP page. Remove calling of copyPending....... 2024-06-12 10:43:48 +08:00
Cindy Chang
d117adf54e fix #287, 261 by removing Math.floor() 2024-06-12 09:05:46 +08:00
Cindy Chang
f1cabe2035 fix: #294 by adding next param 2024-06-11 16:01:29 +08:00
Cindy Chang
ba418abbef minor fix of typo 2024-06-11 15:31:12 +08:00
Cindy Chang
82fe104291 WIP #216. QA mentioned that bugs remained, so we keep working on it. 2024-06-11 13:55:00 +08:00
Cindy Chang
314670eafd fix #216. Add base5
encode. Also add cypress.
2024-06-11 12:23:17 +08:00
Cindy Chang
aa1dbfabf2 #216 done by using return-to URL mechanism 2024-06-11 11:57:33 +08:00
Cindy Chang
444b7d6975 WIP: not sure what is aaa and bbb meaning in /rule/log/aaa/conformance/bbb 2024-06-11 09:15:16 +08:00
Cindy Chang
d6cb329a5c fix #258. Compare page and Performance pages are featured with formatted x ticks.
The author supposes this issue is not a bug but a feature.
2024-06-07 15:55:51 +08:00
Cindy Chang
7e362d8740 fix #217; this time finally found the root cause.
If user didn't click any start-end radio button this time,
the start, end value might be null as their initial values are.
So we need to use the earliest value stored in pinia. (In ActRadio.vue created phase)
2024-06-07 15:31:53 +08:00
Cindy Chang
88082c3b8a #217 fix. The bug is solved without finding root cause. Maybe it's just because the author force the website to print out the variable (this.selectCfmPtPSEStart) 2024-06-07 11:56:41 +08:00
Cindy Chang
e16304df45 WIP #217 don't know why it seems to work though 2024-06-07 11:39:53 +08:00
Cindy Chang
fc99bac449 WIP customizing x tick according to time difference 2024-06-07 10:37:28 +08:00
Cindy Chang
92160fab54 typo fix 2024-06-07 08:55:39 +08:00
Cindy Chang
29675af82b add setTimeStringFormatBaseOnTimeDifference 2024-06-07 08:54:21 +08:00
Cindy Chang
c0c455c55c refine performance code layout 2024-06-06 15:16:43 +08:00
Cindy Chang
9539237c3d refactor (slight, although many files) 2024-06-06 09:25:26 +08:00
Cindy Chang
0df045c218 fix #294 by handling navItem string with function mapPageNameToCapitalUnifiedName() 2024-06-05 14:42:24 +08:00
Cindy Chang
5f0e12ef1a pageAdmin add setActivePageComputedByRoute function 2024-06-05 13:08:52 +08:00
Cindy Chang
427b9b6aed refactor: rename isSubmit --> isAlreadySubmit 2024-06-05 11:05:55 +08:00
Cindy Chang
205b75450c refactor conformance related code 2024-06-05 10:50:37 +08:00
Cindy Chang
499ad33d57 locale of Conformance page 2024-06-05 09:46:20 +08:00
Cindy Chang
e2d420b4bd add unused getYAxisTickMark(); this is written by ChatGPT 2024-06-05 09:04:23 +08:00
Cindy Chang
5418a6e7c0 solving toUpperCase bug 2024-06-04 09:17:55 +08:00
Cindy Chang
265680ae45 WIP: refactoring Compare page 2024-06-03 16:48:22 +08:00
Cindy Chang
55aaab3672 compare page locale continue 2024-06-03 15:10:17 +08:00
Cindy Chang
90eba51a38 compare page locale 2024-06-03 14:50:49 +08:00
Cindy Chang
cf7c9025ac feature: i18next first sentence done 2024-06-03 12:45:15 +08:00
Cindy Chang
dfd3199d4c WIP i18next 2024-06-03 12:41:30 +08:00
Cindy Chang
9a5dd2d786 refine only layout of code 2024-06-03 12:04:35 +08:00
Cindy Chang
cc3fa5df8d unused cypress file; why unused? because cypress doesn't support canvas element 2024-06-03 10:15:43 +08:00
Cindy Chang
38e1aa9446 refine and clean console.log 2024-05-31 16:07:52 +08:00
Cindy Chang
2142399f8c fix #295 include both save and cancel cases of popup modal; however, for cases when conformance page acting as starting point, bugs are still there 2024-05-31 16:04:52 +08:00
Cindy Chang
0a1eefbaa7 original bug is fixed but save modal is not handle (nav-item bg color is not correctly painted) 2024-05-31 14:36:51 +08:00
Cindy Chang
69f6a2048a WIP #295 not yet keep previous page 2024-05-31 12:03:09 +08:00
Cindy Chang
83f399b746 cypress: #288 add missed chart 2024-05-30 14:16:31 +08:00
Cindy Chang
6ff09920c6 fix #288 missed "cycle time & efficiency" chart on Compare page 2024-05-30 11:07:29 +08:00
chiayin
f2bdfe0cec release: Update to v1.0.0 2024-03-29 15:51:35 +08:00
chiayin
e391c9a237 docs: Update all files JSDoc. 2024-03-29 15:49:05 +08:00
chiayin
4ba71d1193 docs: Update the Github's README. 2024-03-29 11:22:21 +08:00
chiayin
683c407c6e docs: structure tree update. 2024-03-29 10:31:30 +08:00
chiayin
301d49bd08 fix: Issues #218 done. 2024-03-28 16:34:06 +08:00
chiayin
c608b1cc52 fix: Issues #278 done. 2024-03-27 11:20:54 +08:00
chiayin
394b535118 fix: Issues #218, Time value Change done. 2024-03-26 17:08:51 +08:00
chiayin
ef3bab3592 fix: Issues #217, Time Picker Change done. 2024-03-26 12:54:47 +08:00
chiayin
5acbabfa1c fix: Issues #217, start & end done. 2024-03-25 17:19:12 +08:00
chiayin
c26693f933 fix: Issues #262 done. 2024-03-25 12:03:23 +08:00
chiayin
83cca58fe2 fix: Issues #261 done. 2024-03-25 11:36:06 +08:00
chiayin
dfc0d9a7a6 fix: Issues #230 done. 2024-03-22 15:42:20 +08:00
chiayin
af2e9c75e3 fix: Issues #243 change timer to 3 sec done. 2024-03-21 15:57:14 +08:00
chiayin
3cd8cbc973 fix: Issues #243 uploadFailedSecond done. 2024-03-21 15:51:38 +08:00
chiayin
2de6089f61 fix: Issues #276 done. 2024-03-21 12:15:00 +08:00
chiayin
30b8b45b1f fix: Issues #274 done. 2024-03-21 11:40:44 +08:00
chiayin
41ae85c12e fix: Issues #270 done. 2024-03-21 11:39:06 +08:00
chiayin
d009459747 fix: Issues #273 done. 2024-03-21 11:37:51 +08:00
chiayin
b9ddd7ee66 fix: Issues #275 done. 2024-03-21 11:33:38 +08:00
chiayin
5d6b0c8253 fix: Issues #277 Compare page done. 2024-03-21 11:27:21 +08:00
chiayin
456078141a fix: Issues #277 Performance page done. 2024-03-21 11:25:37 +08:00
chiayin
68c9e1569c fix: UPLOAD page UI done. 2024-03-20 12:15:35 +08:00
chiayin
dac4f5bb8c fix: Issues #213 done. 2024-03-20 11:05:58 +08:00
chiayin
483f6a026d refactor: Discover Router Save done. 2024-03-19 16:29:29 +08:00
chiayin
6663b48159 refactor: Discover Router done. 2024-03-19 15:57:12 +08:00
chiayin
bc3e20abd0 refactor: Conformance Router - FILES page to Confomance page router refresh the page done. 2024-03-15 18:10:25 +08:00
chiayin
49b0e462a1 refactor: Conformance Router - FILES page to Confomance page router done. 2024-03-15 16:52:43 +08:00
chiayin
1b813584c0 fix: Issues #246 done. 2024-03-14 16:42:39 +08:00
chiayin
fd49c8a414 fix: Issues #211 done. 2024-03-13 17:09:12 +08:00
chiayin
6162155997 fix: Issues #252 done. 2024-03-13 16:24:03 +08:00
chiayin
dfb5aee761 fix: Issues #259 done. 2024-03-13 15:38:42 +08:00
chiayin
4cea216337 fix: Issues #261 done. 2024-03-13 12:16:10 +08:00
chiayin
2c5eb7bcea fix: Issues #252 remove table-fixed done. 2024-03-12 17:33:40 +08:00
chiayin
8605f80c79 fix: Issues #258 Compare Line Chart done. 2024-03-12 16:55:24 +08:00
chiayin
689e792cbc fix: Issues #258 Performance Line Chart done. 2024-03-12 16:54:24 +08:00
chiayin
0cfbb33fc8 fix: Issues #256 done. 2024-03-12 16:19:52 +08:00
chiayin
e581bdcfe4 fix: Issues #256 fix Compare Line Chart Y unit to 'Count' done. 2024-03-12 16:16:02 +08:00
chiayin
64ae986384 fix: Issues #256 fix Performance Line Chart Y unit to 'Count' done. 2024-03-12 15:47:15 +08:00
chiayin
2c6a45232d fix: Issues #256 fix Compare Horizontal Bar Chart X unit to 'Count' done. 2024-03-12 14:09:28 +08:00
chiayin
fa0376ec5f fix: Issues #256 fix Horizontal Bar Chart X unit to 'Count' done. 2024-03-12 12:10:49 +08:00
chiayin
14cd3bd986 fix: Issues #253 fixed Compare titel font-size done. 2024-03-12 11:14:10 +08:00
chiayin
557f0812c7 fix: Issues #253 fixed Performancce titel font-size done. 2024-03-12 11:11:22 +08:00
chiayin
e28aaf9cce fix: Issues #222 done. 2024-03-07 17:34:19 +08:00
chiayin
e8dc258c12 fix: Issues #138 files page thead sticky and only tbody have scrollbar done. 2024-03-07 14:59:54 +08:00
chiayin
c0ad84169a fix: Issues #138 files page thead sticky and only tbody have scrollbar done. 2024-03-07 12:29:08 +08:00
chiayin
5b62799efb fix: Issues #221 impge import src done. 2024-03-06 16:19:55 +08:00
chiayin
e6e99ab78b fix: Issues #209 done. 2024-03-06 15:36:54 +08:00
chiayin
91e46448b0 fix: Issues #60 done. 2024-03-06 12:17:36 +08:00
chiayin
5df24139bd fix: Issues #250 done. 2024-03-05 17:06:23 +08:00
chiayin
0b0bc3aa54 fix: Issues #249 done. 2024-03-05 16:56:18 +08:00
chiayin
d333453578 fix: Issues #242 done. 2024-03-05 16:07:22 +08:00
chiayin
50ff27f377 fix: Issues #241 done. 2024-03-05 15:22:37 +08:00
chiayin
e622f1b6fe fix: Issues #240 done. 2024-03-05 15:21:24 +08:00
chiayin
0503da4a3d fix: Issues #238 done. 2024-03-05 14:21:20 +08:00
chiayin
529282c2a4 fix: Issues #228 done. 2024-03-05 12:46:25 +08:00
chiayin
b37e452a73 fix: Issues #225 done. 2024-03-05 11:02:36 +08:00
chiayin
e8af9f93ec fix: Issues #223 done. 2024-03-04 17:19:17 +08:00
chiayin
aad3e998f6 fix: Issues #187 done. 2024-03-04 16:48:00 +08:00
chiayin
0ce02f7587 fix: Issues #47 done. 2024-03-04 11:01:59 +08:00
chiayin
deeee05647 fix: Issues #220 done. 2024-03-04 10:51:34 +08:00
chiayin
647d9ff59d fix: Issues #237 done. 2024-03-01 17:03:52 +08:00
chiayin
0d12c50a1b fix: Issues #234 done. 2024-03-01 17:01:18 +08:00
chiayin
734ab15f68 fix: Issues #231 content done. 2024-03-01 16:55:03 +08:00
chiayin
68cf73f4e5 fix: Issues #231 done. 2024-03-01 16:53:30 +08:00
chiayin
20bfa0b2ef fix: Issues #221 done. 2024-03-01 16:41:49 +08:00
chiayin
62e1d37f37 fix: Issues #224 done. 2024-03-01 16:18:07 +08:00
chiayin
4704b0deef fix: Issues #149 thead to text-center done. 2024-03-01 10:49:30 +08:00
chiayin
2a8548170b fix: Issues #149 tr to text-center done. 2024-03-01 10:45:23 +08:00
chiayin
14c3a2b05c fix: Issues #47 info icon done. 2024-02-29 17:18:55 +08:00
chiayin
b01937e491 fix: Issues #30 done. 2024-02-29 11:57:36 +08:00
chiayin
bec8184620 fix: router beforeEach done. 2024-02-29 10:53:42 +08:00
chiayin
695bd767c8 fix: Issues #201 done. 2024-02-26 16:40:36 +08:00
chiayin
d416e2d8de fix: Issues #197 done. 2024-02-26 16:32:53 +08:00
chiayin
c590aa3531 fix: Issues #194 done. 2024-02-26 16:29:26 +08:00
chiayin
c5a28cb161 fix: Issues #51 done. 2024-02-26 15:27:37 +08:00
chiayin
fcf640cc5b fix: Issues #33 done. 2024-02-26 15:04:40 +08:00
chiayin
1ab59a859b fix: 404 page back router done. 2024-02-23 17:11:19 +08:00
chiayin
ffd13ad1f7 tag: v0.99.9 2024-02-23 16:41:59 +08:00
chiayin
9f9437f303 feat: Compare no wait time layout done. 2024-02-23 16:40:52 +08:00
chiayin
0f7945c6fe test: Compare dropdown sort done. 2024-02-23 15:24:25 +08:00
chiayin
cec820258b fix: Compare SidebarStates File Name line-hight done. 2024-02-22 16:59:20 +08:00
chiayin
b6bd1e878e test: Enter Compare & Anchor done. 2024-02-22 16:51:49 +08:00
chiayin
f7c76fd1f7 feat: Performance not show SAVE button done. 2024-02-22 10:42:40 +08:00
chiayin
d18673acc8 fix: file grid card line-height done. 2024-02-22 10:38:07 +08:00
chiayin
d921d9fa75 feat: Compare not show SAVE button done. 2024-02-22 10:26:03 +08:00
chiayin
d852d7be06 feat: Compare file sort Dropdown done. 2024-02-22 10:12:45 +08:00
chiayin
f429d21584 fix: Primevue css update to fix google font icon text-size done. 2024-02-22 10:08:40 +08:00
chiayin
df7ac38397 fix: Primevue css update done. 2024-02-21 18:29:52 +08:00
chiayin
a6a007abd2 feat: Compare SidebarStates API done. 2024-02-20 17:40:19 +08:00
chiayin
1eadcdd506 feat: Compare & Performance add function information done. 2024-02-20 12:58:08 +08:00
chiayin
795480ee89 feat: Compare Dashboard API done. 2024-02-20 12:44:30 +08:00
chiayin
01b8a95485 feat: Compare Grid Sort done. 2024-02-19 17:26:18 +08:00
chiayin
66a6f7806d feat: Compare sidebar layout done. 2024-02-16 11:35:10 +08:00
chiayin
ac8a3b0b27 feat: Compare bar layout done. 2024-02-15 13:00:01 +08:00
chiayin
f3dd456b3a test: Performance Anchor done. 2024-02-07 12:27:02 +08:00
chiayin
88de9cfa53 test: Performance Enter Rule done. 2024-02-07 11:41:00 +08:00
chiayin
8ce668f471 feat: Performance conformance page to performance page done. 2024-02-07 11:37:53 +08:00
chiayin
2258f0e7b0 test: Performance Enter Log and Enter Filter done. 2024-02-07 11:21:52 +08:00
chiayin
ac9e13d067 feat: Performance done. 2024-02-06 16:47:54 +08:00
chiayin
c811baefa6 feat: Performance all chart done. 2024-02-05 15:50:33 +08:00
chiayin
5063d49794 feat: Performance Title font normal done. 2024-02-05 13:15:33 +08:00
chiayin
70d2031d61 feat: Performance activity ellipsis done. 2024-02-05 13:13:46 +08:00
chiayin
85d604fc38 feat: Performance Horizontal Bar Height done. 2024-02-05 11:30:12 +08:00
chiayin
c86ce2d6f0 feat: Performance tooltip done. 2024-02-02 16:14:09 +08:00
chiayin
7c959e7cd9 feat: Performance dateLabel done. 2024-02-02 15:00:45 +08:00
chiayin
d89c5ff4d8 feat: Performance timeLabel done. 2024-02-01 12:33:11 +08:00
chiayin
bddc1d3a7a feat: Performance aside done. 2024-01-26 17:08:34 +08:00
chiayin
329e1035ad feat: Performance API done. 2024-01-26 10:38:42 +08:00
chiayin
8ebfb151c7 feat: Performance router done. 2024-01-25 15:35:23 +08:00
chiayin
fefb3f325d test: fileUploadEtc testing done. 2024-01-25 11:31:45 +08:00
chiayin
3d58133a04 test: Donwload file. Done. 2024-01-25 11:30:22 +08:00
chiayin
6d6c4cc4ac test: Delete file. Done. 2024-01-25 10:48:40 +08:00
chiayin
56408e8b85 test: Right file upload. Done. 2024-01-24 17:12:35 +08:00
chiayin
be5a7d0c4a test: Phase two worng file upload. Done. 2024-01-24 16:26:59 +08:00
chiayin
04dec3bbd3 test: Upload Page, rename, reset, back to page. Done. 2024-01-24 12:29:35 +08:00
chiayin
a93ee122d0 feat: Refresh Token in processes. 2024-01-22 10:46:24 +08:00
chiayin
c64ba68135 fix: Remove my-account API in main page. 2024-01-18 15:34:17 +08:00
chiayin
003c638d21 feat: Files uploadFailedSecond error text done. 2024-01-18 14:54:18 +08:00
chiayin
e61c458d92 feat: Files delete modal error text done. 2024-01-18 14:44:35 +08:00
chiayin
b23d89bc38 feat: Files uploadFailedSecond error text done. 2024-01-17 16:59:57 +08:00
chiayin
a9f991a715 feat: Files uploadFailedFirst size > 80MB text done. 2024-01-17 11:20:36 +08:00
chiayin
75bbcd07ab feat: Files Delete File done. 2024-01-17 10:46:07 +08:00
chiayin
30a97b2a3b fix: API Error Message text done. 2024-01-16 11:42:25 +08:00
chiayin
5bc9139fc5 fix: Files Upload API & Rename API done. 2024-01-16 11:40:03 +08:00
chiayin
02deb591a1 feat: Refresh Token in progress. 2024-01-16 11:07:59 +08:00
chiayin
9753ed535d feat: File download done. 2024-01-15 17:18:45 +08:00
chiayin
c64d243de2 feat: File list hover rename done. 2024-01-15 13:03:22 +08:00
chiayin
2df2dc718f feat: File table change Collapse style done. 2024-01-15 12:44:25 +08:00
chiayin
d7b372f620 feat: File list hover layout done. 2024-01-15 12:01:22 +08:00
chiayin
d4f0801ca1 feat: File rename API done. 2024-01-11 12:26:40 +08:00
chiayin
01d712e487 feat: Upload no uploadId need to next to files page. 2024-01-08 12:56:34 +08:00
chiayin
080eb6ae95 feat: Upload Navbar done. 2024-01-08 09:12:32 +08:00
chiayin
0535cb5a91 feat: Upload Navbar done. 2024-01-05 17:01:44 +08:00
chiayin
fe5731ae15 feat: Upload failde Second Modal done. 2024-01-05 16:39:09 +08:00
chiayin
68981e98f5 fix: ToastPlugin duratio 5000 done. 2024-01-05 15:24:18 +08:00
chiayin
3fb18d4a17 feat: upload Rename Input done. 2024-01-05 14:24:12 +08:00
chiayin
65c42312ee feat: upload contentEditable done. 2024-01-03 17:45:46 +08:00
chiayin
16d9547305 fix: inform judgment add blank text. 2023-12-29 16:55:26 +08:00
chiayin
96263567e6 fix: tooltipUpload add opacity. 2023-12-29 16:39:03 +08:00
chiayin
9ef441ee83 feat: upload done. 2023-12-29 16:05:27 +08:00
chiayin
cd2ab42125 test: update E2E testing. 2023-12-15 12:26:32 +08:00
chiayin
90610c173f feat: Upload doing. 2023-12-15 11:32:48 +08:00
chiayin
911abc2139 refactor: Issues #177 change files API done. 2023-12-14 15:36:50 +08:00
chiayin
9c7fd4a202 feat: Upload layout done. 2023-12-14 09:34:35 +08:00
chiayin
67b0ee47df feat: Upload Alert done. 2023-12-12 11:56:21 +08:00
chiayin
7ed6f97857 release: tag v0.99.7 2023-12-08 14:57:43 +08:00
chiayin
8615834d2a test: E2E-conformance Waiting time and Cycle time done. 2023-12-08 12:57:40 +08:00
chiayin
24212f9a3a test: E2E-conformance rule Processing time All done. 2023-12-08 12:28:58 +08:00
chiayin
9d52db406d test: E2E-conformance rule Processing time E2E All done. 2023-12-07 17:26:44 +08:00
chiayin
44fe8acc8f test: E2E-conformance rule Activity duration done. 2023-12-07 17:16:58 +08:00
chiayin
75d1d43738 test: E2E-conformance rule Activity duration done. 2023-12-07 17:13:09 +08:00
chiayin
2403024d33 test: E2E-conformance rule Activity sequence done. 2023-12-07 16:25:31 +08:00
chiayin
7a98f549b6 test: E2E-conformance rule Have activity. 2023-12-06 17:26:55 +08:00
chiayin
31c9143aa6 test: E2E-conformance no save done. 2023-12-06 15:57:49 +08:00
chiayin
a745015442 test: E2E-conformance save log and filter done. 2023-12-06 12:52:50 +08:00
chiayin
71f0926eb9 fix: Conformance Save Sequence radio done. 2023-12-04 15:01:08 +08:00
chiayin
8ae4082a47 feat: Conformance Save leaved page done. 2023-12-04 14:11:29 +08:00
chiayin
8b502e67f4 feat: Conformance Save Logout done. 2023-12-01 14:24:05 +08:00
chiayin
c26a1dfee7 feat: Conformance Save switchNavItem done. 2023-11-30 12:19:46 +08:00
chiayin
95c429ef00 feat: Conformance Save Filter Done. 2023-11-30 11:26:01 +08:00
chiayin
1f0a6aa900 feat: Conformance Save Log Done. 2023-11-29 16:47:25 +08:00
chiayin
f1666a0bd1 fix: Issues #208 done. 2023-11-23 13:43:15 +08:00
chiayin
ec596ef3ed fix: Issues #192 done. 2023-11-22 17:16:48 +08:00
chiayin
081ab794aa fix: Issues #108 done. 2023-11-22 13:00:22 +08:00
chiayin
c3d9a3d9e4 fix: Issues #198 done. 2023-11-22 11:40:28 +08:00
chiayin
8949476e60 fix: Issues #203 done. 2023-11-21 11:43:13 +08:00
chiayin
5c25b37d51 Merge branch 'main' of github.com:dspim/lucia-frontend 2023-11-21 10:22:51 +08:00
chiayin
b0b1c32932 fix: Issues #200 done. 2023-11-21 10:21:22 +08:00
chiayin
3254a24740 fix: Issues #203 done. 2023-11-20 15:01:06 +08:00
chiayin
1aba349a76 Tag: add v0.99.6 2023-11-13 13:58:42 +08:00
chiayin
c323fbb28e Issues #161: done. 2023-11-13 13:57:13 +08:00
chiayin
b92ddbc83b Issues #162: done. 2023-11-13 13:34:51 +08:00
chiayin
918de38fcc Tag: add v0.99.5 2023-11-13 13:04:47 +08:00
chiayin
00a1931189 Issues #191: done. 2023-11-13 13:02:06 +08:00
chiayin
dd7057c2b1 Issues #139: done. 2023-11-13 09:52:26 +08:00
chiayin
6c81eff7a5 Issues #142: done. 2023-11-13 09:45:48 +08:00
chiayin
0254209053 Issues #172: done. 2023-11-13 09:30:47 +08:00
chiayin
91045c0706 Issues #188: done. 2023-11-13 09:10:15 +08:00
chiayin
79385c2e3c Tag: add v0.99.4 2023-11-10 16:44:53 +08:00
chiayin
ec841511be Issues #11: done. 2023-11-10 15:51:52 +08:00
chiayin
7f5f8a10be Issues #36: done. 2023-11-10 14:56:52 +08:00
chiayin
6f9d57baba Issues #108: done. 2023-11-10 14:51:42 +08:00
chiayin
4ee566d7ad Issues #141: done. 2023-11-10 14:47:50 +08:00
chiayin
0e6bbd2e20 Issues #152: done. 2023-11-10 14:44:58 +08:00
chiayin
99e30a1adc Issues #153: done. 2023-11-10 14:43:42 +08:00
chiayin
66372e4d93 Issues #153: done. 2023-11-10 14:41:57 +08:00
chiayin
9118497f09 Issues #182: done. 2023-11-10 14:19:43 +08:00
chiayin
03cacd5a9b Issues #183: done. 2023-11-10 11:51:15 +08:00
chiayin
edd56aa001 Issues #190: done. 2023-11-10 10:47:31 +08:00
chiayin
e7c041d5f1 Merge branch 'main' of github.com:dspim/lucia-frontend 2023-11-09 16:58:23 +08:00
chiayin
3fe92de61e Issues #183: done. 2023-11-09 16:57:12 +08:00
chiayin
4f8e6dc260 Issues #186: done. 2023-11-09 16:50:52 +08:00
chiayin
d288c8f68e Issues #186: fix, The sidevar needs to close when the button is pressed. 2023-11-06 16:12:18 +08:00
chiayin
5d958f672f Issues #186: done. 2023-11-06 15:50:26 +08:00
chiayin
d21d4ed16b Issues #182: done. 2023-11-06 14:49:25 +08:00
chiayin
672d93b821 Issues #179: done. 2023-11-03 17:12:57 +08:00
chiayin
59f59e6280 Fix: add Attributes boolean type freq. 2023-11-03 13:14:23 +08:00
chiayin
cbb1f7c446 Issues #171: add filter sort done. 2023-11-02 18:00:01 +08:00
chiayin
0be3bcfdd0 Issues #174: done. 2023-11-02 17:42:43 +08:00
chiayin
89f0ef9a5a Issues #171: done. 2023-11-01 11:23:52 +08:00
chiayin
4089b1c7b8 Issues #170: done. 2023-11-01 11:17:45 +08:00
chiayin
1896b8d8cf Issues #175: done. 2023-10-31 17:17:17 +08:00
chiayin
a4ea3bff5d Map Attributes: fix allMapData.js. 2023-10-31 16:38:54 +08:00
chiayin
64fad0a8fe Map Attributes: fix allMapData.js. 2023-10-31 16:37:25 +08:00
chiayin
19c536a4f2 Map Timeframe: fix not mask.style error. 2023-10-31 16:06:00 +08:00
chiayin
9d7f390343 Map Timeframe: fix second Apply need to disabled. 2023-10-31 15:35:12 +08:00
chiayin
2ac112dbbd Map Attributes: Apply, Clear done. 2023-10-31 15:29:18 +08:00
chiayin
fdba0abc37 Map Attributes: API Format converter. 2023-10-30 16:10:37 +08:00
chiayin
03442f1934 Map Attributes: value type int and float done. 2023-10-30 10:06:54 +08:00
chiayin
bca9978fad Map Attributes: value type int and float Chart.js done. 2023-10-26 12:50:59 +08:00
chiayin
7eb9b1c63d Map Attributes: value type Calendar done, but have event bubbling error. 2023-10-25 17:34:14 +08:00
chiayin
7700d60546 Map Attributes: value type Slider done. 2023-10-25 11:13:37 +08:00
chiayin
937c1459d7 Map Attributes: type 'string' layout done. 2023-10-23 15:40:36 +08:00
chiayin
e55cbc013c Map Attributes: type 'boolean' layout done. 2023-10-23 13:31:33 +08:00
chiayin
6c032ca94a Issue #169: Done. 2023-10-19 14:11:40 +08:00
chiayin
b32e1b3b61 Conformance Filter: done. 2023-10-19 11:01:21 +08:00
chiayin
1fd019d276 Oauth: add grant_type. 2023-10-19 10:12:36 +08:00
chiayin
06531d0acf Conformance Filter: fetch filter doning 50%. 2023-10-19 10:12:18 +08:00
chiayin
830f4d30a9 Conformance Filter: fetch filter params API done. 2023-10-18 12:40:51 +08:00
chiayin
85d1f9a487 Issue #151: Done. 2023-10-17 15:52:38 +08:00
chiayin
c85b7f2d71 Issue #148: Done. 2023-10-17 09:59:09 +08:00
chiayin
4c46347127 Issue #148: Done. 2023-10-16 18:53:48 +08:00
chiayin
946fabfa93 Issue #140: 2023-10-16 18:38:20 +08:00
chiayin
e751aed3eb Issue #112: Done. 2023-10-13 18:33:49 +08:00
chiayin
cf3d799ed7 Issue #140: Done. 2023-10-13 17:58:47 +08:00
chiayin
1cfbddf510 Issue #165: Done. 2023-10-11 18:50:46 +08:00
chiayin
6e0d5b15fe Issue #168: Done. 2023-10-06 15:39:16 +08:00
chiayin
6ffd676182 Issue #132: Done. 2023-10-06 12:31:26 +08:00
chiayin
6bd92dfef3 Issue #160: Done. 2023-10-06 11:57:36 +08:00
chiayin
a7f51e3444 Issue #144: Done. 2023-10-05 17:20:35 +08:00
chiayin
897f8d0a41 Issue #45: fix all conformance, not show Not-conformance Issues title Done. 2023-10-05 16:22:14 +08:00
chiayin
a93fca36f5 Issue #166: Done. 2023-10-04 16:54:19 +08:00
chiayin
8a9f6ecd2e Issue #154: Done. 2023-10-04 15:27:43 +08:00
chiayin
8fda975228 Issue #8: Done. 2023-10-04 15:17:54 +08:00
chiayin
b5f6c9785a Issue #7: Done. 2023-10-04 15:14:15 +08:00
chiayin
40b339a235 Issue #3: Done. 2023-10-04 15:04:51 +08:00
chiayin
c845d0674a Issue #2: Done. 2023-10-04 15:00:42 +08:00
chiayin
a41a43e688 Issue #9: Done. 2023-10-04 14:26:16 +08:00
chiayin
3ef7b2502e Issue #10: Done. 2023-10-04 14:07:41 +08:00
chiayin
2b07f965cf Issue #12: Done. 2023-10-04 14:06:24 +08:00
chiayin
240f68a73e Issue #14: Done. 2023-10-04 14:04:45 +08:00
chiayin
a25290f4db Issue #17: Done. 2023-10-04 14:02:00 +08:00
chiayin
c7e706dd5b Issue #22: Done. 2023-10-04 13:55:11 +08:00
chiayin
ddd8df9daa Issue #31: Done. 2023-10-04 13:46:51 +08:00
chiayin
803782d5a9 Issue #34: Done. 2023-10-04 11:01:17 +08:00
chiayin
bee668a3f7 Issue #104: Done. 2023-10-03 16:57:21 +08:00
chiayin
112c4d1cdf Issue #107: Done. 2023-10-03 16:01:21 +08:00
chiayin
285fb43aa3 Issue #114: Done. 2023-10-03 12:32:27 +08:00
chiayin
436586d6e9 Issue #124: Done. 2023-10-02 17:19:30 +08:00
chiayin
f9738bbed9 Issue #122: Done. 2023-10-02 15:18:48 +08:00
chiayin
20c7e4316b Issue #123: Done. 2023-09-28 14:38:32 +08:00
chiayin
64c8aec657 Issue #145: Add padding-bottom: 5rem; 2023-09-28 13:01:53 +08:00
chiayin
ed4314e6c0 Issue #164: Done. 2023-09-28 12:57:20 +08:00
chiayin
18f376c734 Issue #158: Done. 2023-09-28 12:49:32 +08:00
chiayin
5ea268cc77 Issue #94: Second Done. 2023-09-28 12:37:31 +08:00
chiayin
a8f47f7593 Issue #148: Done. 2023-09-27 16:25:04 +08:00
chiayin
e14ec4bef2 Issue #151: Done. 2023-09-27 15:42:08 +08:00
chiayin
aa176f4c4c Issue #150: Done. 2023-09-27 14:57:23 +08:00
chiayin
1a533d1376 Issue #156: Done. 2023-09-27 11:37:47 +08:00
chiayin
acc89bf301 Issue #155: Done. 2023-09-27 11:36:42 +08:00
chiayin
3a50b9ba8f Issue #130: Done. 2023-09-27 10:27:38 +08:00
chiayin
cb117979a2 Merge branch 'main' of github.com:dspim/lucia-frontend 2023-09-27 10:21:47 +08:00
chiayin
7617af8159 Issue #68: Second point done. 2023-09-27 10:17:50 +08:00
chiayin
56a808c581 tag: V0.99.1.01 2023-09-26 13:48:09 +08:00
chiayin
442a7168b6 Issue #93: Done. 2023-09-26 13:40:51 +08:00
chiayin
349e565696 Issue #80: Done. 2023-09-25 18:26:30 +08:00
依瑪貓
453219f569 Renamed "conformance checker" to "conformance check". 2023-09-23 22:53:32 +08:00
chiayin
add059f1e1 Issue #39: change Rule cleared. 2023-09-23 17:17:10 +08:00
chiayin
76c5c71f0d Issue #62: change Rule cleared. 2023-09-23 17:08:07 +08:00
chiayin
8f46878ade Issue #59: Done. 2023-09-23 17:06:47 +08:00
chiayin
9cb9b5cb71 Issue #41: Done. 2023-09-22 16:50:05 +08:00
chiayin
b6a11f5ff9 Issue #49: Delete sortable. 2023-09-22 11:02:34 +08:00
chiayin
9a4b3c4180 Issue #137: Delete sortable. 2023-09-22 10:57:42 +08:00
chiayin
9693496011 Issue #62: Done. 2023-09-21 17:56:29 +08:00
chiayin
9338226c0a Issue #64: Done. 2023-09-21 17:54:59 +08:00
chiayin
013a459fed Issue #79: Done. 2023-09-21 17:52:06 +08:00
chiayin
c73ef5d4ba Issue #136: Done. 2023-09-21 17:19:27 +08:00
chiayin
040e8b707a Issue #133: Done. 2023-09-21 15:10:29 +08:00
chiayin
0d930238dd Issue #97: Done. 2023-09-21 12:04:58 +08:00
chiayin
bcda6345c6 Issue #98: Done. 2023-09-20 16:16:44 +08:00
chiayin
28d94db141 Issue #116: Done. 2023-09-20 16:12:44 +08:00
chiayin
d1d0585269 Issue #117: Done. 2023-09-19 17:53:24 +08:00
chiayin
f73979d263 Issue #135: Done. 2023-09-19 17:03:40 +08:00
chiayin
5740241bef Issue #118: Done. 2023-09-19 14:40:40 +08:00
chiayin
c4373ac248 Issue #119: Done. 2023-09-19 14:35:09 +08:00
chiayin
b528e36a8f Issue #121: Done. 2023-09-19 14:26:46 +08:00
chiayin
53ed4938c6 Issue #125: Done. 2023-09-19 14:01:24 +08:00
chiayin
1d20c53f05 Issue #128: Done. 2023-09-19 13:57:30 +08:00
chiayin
9d43e1178f Issue #130: Done. 2023-09-19 13:33:32 +08:00
chiayin
728d99d84e Issue #134: Done. 2023-09-19 13:30:20 +08:00
chiayin
29b434e672 Issue #134: Done. 2023-09-19 13:29:15 +08:00
chiayin
6c2783cd5d Issue #77: fix show test. 2023-09-19 12:46:32 +08:00
chiayin
53e23b9475 Issue #77: done. 2023-09-18 17:28:58 +08:00
chiayin
e1a9e57468 Issue #76: done. 2023-09-15 14:58:40 +08:00
chiayin
c00123ddcb Issue #53: done. 2023-09-14 21:01:09 +08:00
chiayin
47ca3d8f21 Issue #68: done. 2023-09-13 17:40:51 +08:00
chiayin
86940d6887 Conformance: fix ActSeqDrag double click feature. 2023-09-13 16:02:44 +08:00
chiayin
712ccb647e Issue #45: done. 2023-09-13 12:02:15 +08:00
chiayin
4b0cf87414 Issue #102: done. 2023-09-13 11:47:09 +08:00
chiayin
7e48007e4e Issue #89: done. 2023-09-12 17:43:31 +08:00
chiayin
ce49422a86 Issue #94: done. 2023-09-12 16:59:20 +08:00
chiayin
83a6f072b4 Issues #71: Short loop(s)、Self loop(s), ALL, Done. 2023-09-12 14:59:47 +08:00
chiayin
11eb320c55 Issues #83: done. 2023-09-12 11:48:32 +08:00
chiayin
35a494b99e Issues #71: Short loop(s)、Self loop(s) done. 2023-09-11 16:53:48 +08:00
chiayin
3a7d5429a7 Issues #65: done. 2023-09-11 11:54:08 +08:00
chiayin
5cea9b209b Issues #58: done. 2023-09-11 11:36:26 +08:00
chiayin
53a1febeeb Conformance: delete results test object. 2023-09-11 11:21:25 +08:00
chiayin
a2591db2ab Conformance: Change backend API float-list to duration-list done. 2023-09-11 11:03:19 +08:00
chiayin
c2592a6bc4 Issues #44: done. 2023-09-11 09:53:45 +08:00
chiayin
81b1bd905b Fix: Have activity && Activity sequence not show Time Range. 2023-09-05 16:43:06 +08:00
chiayin
8351a33f90 Fix Issues: #42 Case Duration Value done. 2023-09-05 16:25:49 +08:00
chiayin
f98b1b570e Conformance: done. 2023-09-05 15:37:45 +08:00
chiayin
ef6c475782 Conformance: fix Time Durationjs max Val and totalVal. 2023-09-05 15:36:26 +08:00
chiayin
72ec78ab0e Conformance: Time Range show after Apply time done. 2023-09-05 14:59:30 +08:00
chiayin
e3b8153a8e Conformance: add ConformanceTimeRange.vue 2023-09-05 14:06:46 +08:00
chiayin
5219636eb8 Conformance: PT & WT & CT show after Apply Activity Selector tasks done. 2023-09-04 17:19:20 +08:00
chiayin
a587e0aaa6 Conformance: PT & WT & CT show after Apply List tasks done. 2023-09-04 16:54:11 +08:00
chiayin
9c53e298be Conformance: fix ConformanceResults show or not Time Trend. 2023-09-04 12:26:16 +08:00
chiayin
60182c3148 Conformance: fix Time Trend conformance chart blue color done. 2023-09-04 11:32:25 +08:00
chiayin
f68eafca71 Conformance: fix Time Trend xVal data done. 2023-09-04 10:47:44 +08:00
chiayin
a0c2d84639 Conformance: fix Time Trend chart done. 2023-09-04 10:41:57 +08:00
chiayin
3f0d1b9e05 Conformance: Time Trend chart done. 2023-09-04 10:22:06 +08:00
chiayin
5f16c4ac58 Conformance: Have activity & Activity sequence done. 2023-09-01 12:15:46 +08:00
chiayin
49230c1b51 Conformance: Time Trend Chart incomplete. 2023-09-01 11:33:38 +08:00
chiayin
7ee1dac5b1 Conformance: Time Trend Chart Scales-y Option done. 2023-08-31 15:15:08 +08:00
chiayin
b7c04c9382 Conformance: fix Activity sequence Sequence Mode rules. 2023-08-30 14:57:00 +08:00
chiayin
187ce7afcc Conformance: Activity sequence Start & End linkage done. 2023-08-30 14:19:20 +08:00
chiayin
1e49e11a1b Conformance: Time Range done. 2023-08-29 15:47:31 +08:00
chiayin
2b90c02a8c Conformance: time duration component add a comment. 2023-08-29 14:47:53 +08:00
chiayin
89aa346b8f Conformance: time duration component done. 2023-08-29 12:56:22 +08:00
chiayin
b897b163aa Conformance: time duration component fix bug. 2023-08-28 17:01:26 +08:00
chiayin
83485fa8aa Conformance: processing time, waiting time, cycle time, add show activity selector not done. 2023-08-28 09:30:08 +08:00
chiayin
45e7d5a0a1 Conformance: processing time, waiting time, cycle time, add not select show toast done. 2023-08-24 16:25:05 +08:00
chiayin
0850aa0771 Conformance: processing time, waiting time, cycle time, get apply report don. 2023-08-24 15:13:14 +08:00
chiayin
70b4193275 Conformance: processing time, waiting time, cycle time, update all tiem range. 2023-08-23 18:01:37 +08:00
chiayin
ca359436e4 Conformance: processing time, waiting time, cycle time, change radio done. 2023-08-23 16:34:19 +08:00
chiayin
c5de7f17bc Conformance: processing time, waiting time, cycle time, show time Range done. 2023-08-23 10:27:02 +08:00
chiayin
bdbd506734 Conformance: processing time, waiting time, cycle time, showbox list done. 2023-08-22 12:48:58 +08:00
chiayin
66e09aa85c compontent time duration 交互影響, 關聯性待修. 2023-08-21 10:16:24 +08:00
chiayin
c5441475a9 Conformance: Activity duration feature apply done. 2023-08-15 17:37:41 +08:00
chiayin
654e74859a Conformance: fix close modal. 2023-08-15 12:32:45 +08:00
chiayin
c6088cf2ea Conformance: feature update Issues list color. 2023-08-15 11:28:40 +08:00
chiayin
59bbe77fa0 Conformance: feature update ratio Percentage calculation formula. 2023-08-15 10:55:35 +08:00
chiayin
0f10ac6626 Conformance: feature update Conformance Rate Percentage calculation formula. 2023-08-15 10:43:50 +08:00
chiayin
a2473a9ff1 Conformance: Activity sequence Self loop(s) done. 2023-08-15 10:36:53 +08:00
chiayin
82f18ee5e4 Conformance: Activity sequence Short loop(s) done. 2023-08-15 10:31:55 +08:00
chiayin
8f1711de99 featurn: time duration layout done. 2023-08-14 09:32:43 +08:00
chiayin
13e9f7787b featurn: time duration maxValue and minValue done. 2023-08-10 09:44:44 +08:00
chiayin
b84fe38609 Conformance: feature task clear but after apply need to trued in Activity Selector. 2023-08-07 16:25:25 +08:00
chiayin
19da6195ec Conformance: feature task clear but after apply need to trued. 2023-08-04 16:57:54 +08:00
chiayin
34aa0f28c2 Conformance: Activity sequence, Sequence, Directly follows done. 2023-08-02 14:38:08 +08:00
chiayin
86e1c52585 Conformance: fix scrollTop error & Have activity done. 2023-07-28 12:42:28 +08:00
chiayin
864b7bf0ae Conformance: infinite scroll done. 2023-07-28 12:11:31 +08:00
chiayin
765278bada Conformance: backend update Conformance checker API, fix issues section layout. 2023-07-27 11:13:15 +08:00
chiayin
a70a7a96f7 Conformance: backend update Conformance issue API, Conformance report API, fix more model trace table. 2023-07-27 10:29:08 +08:00
chiayin
53038ab2c0 Conformance: Have activity fix chart Date. 2023-07-26 11:41:07 +08:00
chiayin
f2df01e855 Conformance: Have activity More done. 2023-07-25 16:12:46 +08:00
chiayin
dc4ede1d62 Conformance: Have activity Log Results chart, Effect done. 2023-07-20 15:45:22 +08:00
chiayin
97ac9535f9 Conformance: StatusSidebar Have activity Clear button done. 2023-07-13 09:42:58 +08:00
chiayin
6955f376ca Conformance: StatusBar done. 2023-07-12 15:52:03 +08:00
chiayin
9db7b73d2c Conformance: layout done.(not time range) 2023-07-12 09:56:23 +08:00
chiayin
466c6ea867 Scrollbar: change color 2023-06-30 10:37:51 +08:00
chiayin
d976bc529d Conformance: Have activity Layout done. 2023-06-29 15:45:34 +08:00
chiayin
147c9b16cf Conformance: StatusBar done. 2023-06-20 11:51:41 +08:00
chiayin
a6fa85938b fix E2E. 2023-06-17 13:52:42 +08:00
chiayin
af1f8f3016 Router: change /Discover to /Discover/map/type/filterId 2023-06-16 17:13:59 +08:00
chiayin
07b35fcce0 Discover: sidebarFilter Sequence Sequence fix dblclick no need to hide arrow. Update v0.99.1 2023-06-14 12:56:09 +08:00
chiayin
79ab329d87 Discover: sidebarFilter Sequence Sequence add actList sort and add TODO. Update v0.99.1 2023-06-14 12:30:16 +08:00
chiayin
6083bb7988 Organized into a Discover Map version V0.99.1 2023-06-13 18:13:27 +08:00
chiayin
490a2442d8 Radio design done. 2023-06-13 17:54:48 +08:00
chiayin
115626da65 Hide features that don't yet want to do. 2023-06-13 16:17:26 +08:00
chiayin
257eb17ff6 Discover: sidebarFilter - Squence design Drag & Drop UI. 2023-06-13 15:51:37 +08:00
chiayin
faa68d4822 Discover: sidebarFilter - Timeframes fix selectArea bug done. 2023-06-12 12:33:25 +08:00
chiayin
0b2104f41b Cypress: filterFunnel - testing done. 2023-06-12 09:59:02 +08:00
chiayin
77d6c6c56b Cypress: filterFunnel - Sequence Sequence testing done. 2023-06-12 09:53:54 +08:00
chiayin
d9b43b9f4e Discover: sidebarFilter - Sequence -> Sequnce add dblclick teature done. 2023-06-12 09:30:52 +08:00
chiayin
6b3251e9eb Cypress: filterFunnel - Ttmeframes testing done. 2023-06-08 16:38:57 +08:00
chiayin
34f64784a8 Discover: sidebarFilter - fix Timeframes Clear button. 2023-06-08 16:01:53 +08:00
chiayin
a84d123b95 Cypress: filterFunnel - Trace testing done. 2023-06-08 15:17:08 +08:00
chiayin
5600cb074b Cypress: filterFunnel - Start & End testing done. 2023-06-07 17:38:19 +08:00
chiayin
836c3249d9 Discover: sidebarFilter - fix Filter select. 2023-06-07 16:50:57 +08:00
chiayin
cbc330ec5d Cypress: filterFunnel - End testing done. 2023-06-07 16:45:21 +08:00
chiayin
1c364fb15e Cypress: filterFunnel - Have activity & Start testing done. 2023-06-07 16:33:02 +08:00
chiayin
8912ce6e8b Discover: SidebarFilter Timeframes fix startDate & endDate. 2023-06-07 12:19:03 +08:00
chiayin
9a403cce71 Discover: SidebarFilter Timeframes Apply and create Map done. 2023-06-06 14:39:25 +08:00
chiayin
90a5b7532a Discover: SidebarFilter Timeframes div & slider done. 2023-06-01 14:54:39 +08:00
chiayin
9927d34099 Discover: SidebarFilter Timeframes div & slider done. 2023-05-31 17:36:46 +08:00
chiayin
0e510e8270 Discover: SidebarFilter Timeframes fix chart y max value and min value. 2023-05-24 12:49:19 +08:00
chiayin
a9c7723e6e Discover: SidebarFilter delete Containment Trim. 2023-05-24 12:43:31 +08:00
chiayin
6ca0601db7 Discover: SidebarFilter Timeframes slider control to line chart done. 2023-05-23 19:07:11 +08:00
chiayin
f1f2f56d8c Discover: SidebarFilter Trace fix BarChart Option. 2023-05-18 11:55:41 +08:00
chiayin
b37b9638b2 Discover: SidebarFilter Timeframes LineChart Option done. 2023-05-18 11:52:16 +08:00
chiayin
09e5ebf2b2 Discover: SidebarFilter Sequence fix hight. 2023-05-17 11:01:46 +08:00
chiayin
d7e88f6407 Discover: SidebarFilter Trace done. But backend need to check creat Map. 2023-05-17 10:43:50 +08:00
chiayin
64da286748 Discover: SidebarFilter Trace button Clear done. 2023-05-16 18:10:52 +08:00
chiayin
da8a1109be Discover: SidebarFilter Trace button Apply done. 2023-05-16 17:59:45 +08:00
chiayin
895d1bb9b7 Discover: Remove unnecessary code. 2023-05-16 15:47:55 +08:00
chiayin
85514681b7 Discover: SidebarFilter Trace fix traceList trace.ratio value & Trace done. 2023-05-16 15:09:29 +08:00
chiayin
85ed1f84b7 Discover: SidebarFilter Trace Slider control to table and chart done. 2023-05-16 11:17:08 +08:00
chiayin
518842e112 Discover: SidebarFilter Trace table done. 2023-05-15 17:03:28 +08:00
chiayin
0c7765674c Discover: sidebarfilter slider control to chart done. 2023-05-15 16:08:00 +08:00
chiayin
bd18b2c41d Discover: sideFilter Trace Slider done. 2023-05-12 11:13:35 +08:00
chiayin
9fd74b3858 Discover: sideFilter Funnel fix done. 2023-05-08 15:06:28 +08:00
chiayin
ce5e5951b8 cypress-saveLogAndFilter: save filter & no save done. 2023-05-08 14:42:37 +08:00
chiayin
028c58c8ab cypress-saveLogAndFilter: save log done. 2023-05-08 12:51:36 +08:00
chiayin
7daac78bea cypress-saveLogAndFilter: select Activity & checked all event done. 2023-05-04 18:01:28 +08:00
chiayin
739718f3d5 cypress-saveLogAndFilter: post logine api done. 2023-05-04 14:56:45 +08:00
chiayin
162af24a2a Discover: fix filter's save done. 2023-05-03 13:25:18 +08:00
chiayin
51b56c0ccb fix Navbar. 2023-05-02 17:29:48 +08:00
chiayin
c1df89fefb Discover: Filters Save done. 2023-05-02 17:02:05 +08:00
chiayin
378d6ea550 Discover: add Filter Funnel. 2023-04-28 19:57:26 +08:00
chiayin
bf63d0aca4 Files: Filter files can enter Discover. 2023-04-28 11:29:21 +08:00
chiayin
e142787832 Files: change file's icon come from Google front icon. 2023-04-28 09:27:28 +08:00
chiayin
ab8708a597 Discover: Save button done. 2023-04-27 18:40:14 +08:00
chiayin
b1161a82a7 Files: fix accessed_at. 2023-04-27 11:16:33 +08:00
chiayin
cbde7b2d4c Discover: Fix SidebarFilter Funnel. 2023-04-25 10:47:59 +08:00
chiayin
280da0731d Discover: Fix SidebarFilter Funnel. 2023-04-24 17:48:29 +08:00
chiayin
0cff786e9a Discover: sidebarFilter toggle button done. 2023-04-24 12:30:29 +08:00
chiayin
3d5dba4c42 Discover: sidebarFilter Funnel button ApplyAll done. 2023-04-19 16:18:20 +08:00
chiayin
8f68e47c25 set vue-toast-notification css 2023-04-18 18:10:14 +08:00
303 changed files with 48299 additions and 11338 deletions

15
.env Normal file
View File

@@ -0,0 +1,15 @@
# The Lucia project.
# Copyright 2023-2026 DSP, inc. All rights reserved.
# Authors:
# chiayin.kuo@dsp.im (chiayin), 2023/1/31
# imacat.yang@dsp.im (imacat), 2026/3/6
#
# Default environment variables.
# Override in .env.local for local development.
# Backend API URL for the dev server proxy.
VUE_APP_API_URL = ""
# Custom Vite cache directory (default: node_modules/.vite).
# Set to /tmp/vite-cache for systemd deployment with PrivateTmp.
VITE_CACHE_DIR = ""

View File

@@ -1,4 +0,0 @@
/*
For demo env file
*/
VUE_APP_API_URL = "https://REDACTED-HOST/api/"

View File

@@ -1,20 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-prettier",
],
overrides: [
{
files: ["cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}"],
extends: ["plugin:cypress/recommended"],
},
],
parserOptions: {
ecmaVersion: "latest",
},
};

14
.gitignore vendored
View File

@@ -15,10 +15,9 @@ coverage
*.local
/dist
# Cypress
cypress.env.json
/cypress/videos/
/cypress/screenshots/
# Playwright
/test-results/
/playwright-report/
# Editor directories and files
vscode
@@ -32,11 +31,16 @@ vscode
*.njsproj
*.sln
*.sw?
.claude
# local env files
.env.demo
.env.local
.env.*.local
# TypeDoc generated documentation
/docs
.scannerwork
sonar-project.properties
excludes

178
README.md
View File

@@ -1,57 +1,175 @@
# frontend
# The Lucia Project Frontend
This template should help get you started developing with Vue 3 in Vite.
The frontend of the Lucia project, a process mining platform for
analyzing, discovering, and comparing business process workflows.
## Recommended IDE Setup
Built with [Vue 3][vue], [Vite][vite], [Pinia][pinia],
and [Tailwind CSS][tailwind].
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
## Features
See [Vite Configuration Reference](https://vitejs.dev/config/).
- **Files** -- Upload, browse, and manage event log files (CSV).
- **Discover** -- Visualize process maps ([Cytoscape.js][cytoscape]),
analyze performance metrics ([Chart.js][chartjs]), and run
conformance checking against user-defined rules.
- **Compare** -- Side-by-side comparison of process maps and
dashboards.
- **Account Management** -- User account CRUD with role-based
access control.
## Project Setup
## Tech Stack
| Category | Technologies |
|----------|-------------|
| Framework | Vue 3.5, Vue Router 5, Pinia 3 |
| Build | Vite 7, TypeScript 5, Tailwind CSS 4 |
| UI | PrimeVue 4, PrimeIcons, SweetAlert2 |
| Visualization | Cytoscape.js (process maps), Chart.js (charts) |
| HTTP | Axios with JWT refresh token handling |
| Testing | Vitest 4 (unit/component), [Playwright][playwright] (E2E), [MSW][msw] (API mocking) |
| Linting | ESLint, Prettier |
## Prerequisites
- [Node.js][nodejs] (v18 or later)
- npm
## Getting Started
### Install dependencies
```sh
npm install
npm ci
```
### Compile and Hot-Reload for Development
### Configure environment
Copy `.env` to `.env.local` and set the backend API URL:
```sh
cp .env .env.local
```
```sh
# .env.local
VUE_APP_API_URL = "http://localhost:8000"
```
The dev server proxies `/api` requests to this URL.
### Start development server
```sh
npm run dev
```
### Compile and Minify for Production
The app will be available at `http://localhost:58249`.
## Testing
### Run unit and component tests
```sh
npm run build
npx vitest run
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
### Run E2E tests
Build with MSW enabled first, then run [Playwright][playwright]:
```sh
npm run test:unit
```
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
```sh
npm run test:e2e:dev
```
This runs the end-to-end tests against the Vite development server.
It is much faster than the production build.
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
```sh
npm run build
npm run build:e2e
npm run test:e2e
```
### Lint with [ESLint](https://eslint.org/)
For interactive E2E development with the Playwright UI:
```sh
npm run lint
npm run build:e2e
npm run test:e2e:ui
```
## Documentation
Generate API documentation with [TypeDoc][typedoc]:
```sh
npm run docs
```
Output is in the `docs/` directory. Open `docs/index.html` in a
browser to view.
## Build for Production
```sh
npm run build
```
Output is in the `dist/` directory. Preview locally with:
```sh
npm run preview
```
## Project Structure
```
src/
├── api/ # Axios client with JWT interceptors
├── assets/ # CSS (Tailwind), static assets
├── components/ # Reusable Vue components
│ ├── Discover/ # Map, Conformance, Performance
│ ├── Compare/ # Comparison sidebar
│ └── File/ # Upload modal
├── module/ # Business logic (alerts, charts, sorting)
├── router/ # Vue Router configuration
├── stores/ # Pinia stores (state management)
├── utils/ # Utility functions (emitter, escaping)
└── views/ # Page-level route components
├── Discover/ # Map, Performance, Conformance
├── Compare/ # Dashboard, MapCompare
├── Files/ # File browser
├── Upload/ # File upload
└── Login/ # Authentication
```
## Copyright
Copyright 2022-2026 DSP, inc. All rights reserved.
This software is proprietary. You may obtain, use, copy, edit or
update this software with written agreements from DSP, inc.
## Authors
- Chia-Yin Kuo (chiayin.kuo@dsp.im)
- imacat (imacat.yang@dsp.im)
## Acknowledgments
Code quality improvements assisted by [Claude Code][claude-code].
[vue]: https://vuejs.org/
[vite]: https://vitejs.dev/
[pinia]: https://pinia.vuejs.org/
[tailwind]: https://tailwindcss.com/
[cytoscape]: https://js.cytoscape.org/
[chartjs]: https://www.chartjs.org/
[nodejs]: https://nodejs.org/
[playwright]: https://playwright.dev/
[msw]: https://mswjs.io/
[typedoc]: https://typedoc.org/
[claude-code]: https://claude.ai/claude-code

View File

@@ -1,8 +0,0 @@
/* eslint-env node */
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}",
},
});

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["./**/*", "../support/**/*"]
}

View File

@@ -1,44 +0,0 @@
// 之後要優化: 每一個測試步驟要分開寫,不寫在同一個 it 裡面、將測試寫成 report 輸出成 html(嘗試)、功能模組化
const baseUrl = Cypress.env('baseUrl');
describe("Login to Logout", () => {
before(() => {
cy.visit(baseUrl); // 測試可否進入網站
cy.contains("h2", "LOGIN"); // 是否轉址到 /login 並顯示畫面
cy.url().should('include', 'login') // url path 要有 'Login',確定進入 login page
})
it("test login success and error", () => {
// 驗證帳密是否刪除前後空白、錯誤帳密是否顯示驗證、Button display
cy.fixture('users/id-not-exists').then(({username, password}) => {
cy.get('.btn-lg').should('be.disabled');
cy.get('#account').should('have.focus').type(username);
cy.get('.btn-lg').should('be.disabled');
cy.get('#password').type(password);
cy.get('.btn-lg').click();
cy.get('#account').should('have.value', 'test');
cy.get('#password').should('have.value', 'test');
cy.get('form').submit();
cy.contains("p", "Incorrect account or password.");
cy.url().should('include', 'login');
});
// 正確帳密登入
cy.get('#account').clear().type(` ${Cypress.env('user').username} `);
cy.get('#password').clear().type(` ${Cypress.env('user').password} `);
cy.get('.btn-lg').click();
cy.get('#account').should('have.value', Cypress.env('user').username);
cy.get('#password').should('have.value', Cypress.env('user').password);
cy.get('form').submit(); // 選取 form 表單並發送
// 轉址到 files 頁
cy.url().should('include', 'files');
cy.get('a #iconMember').scrollIntoView().click();
// 轉址到會員頁
cy.url().should('include', 'member-area');
// 登出
cy.get('.btn-sm').contains('log out').click();
});
});

View File

@@ -1,4 +0,0 @@
{
"username": " test ",
"password": " test "
}

View File

@@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@@ -1,24 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
Cypress.on('uncaught:exception', (err, runnable) => {
// returning false here prevents Cypress from failing the test
return false
})
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -1,28 +0,0 @@
hello-vite/
├── index.html Vite的進入點
├── node_modules
├── package.json
├── package-lock.json
├── public 不被 JavaScript 引用的靜態資源
│ └── favicon.ico 網頁標題欄 icon
├── README.md
├── src
│ ├── App.vue 網頁根元件
│ ├── main.js 程式進入點
│ ├── assets 靜態資源 EX: 圖片, CSS
│ │ ├── base.css
│ │ ├── logo.svg
│ │ └── main.css
│ ├── components 元件檔
│ │ ├── HelloWorld.vue
│ │ ├── icons (略 - 放所有 icon 的 svg 檔)
│ │ ├── TheWelcome.vue
│ │ └── WelcomeItem.vue
│ ├── router Vue Router 路由管理
│ │ └── index.js
│ ├── stores Pinia 狀態管理器
│ │ └── counter.js
│ └── views 路由元件
│ ├── AboutView.vue
│ └── HomeView.vue
└── vite.config.js vite 設定檔

129
eslint.config.mjs Normal file
View File

@@ -0,0 +1,129 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/3/6
/**
* @module eslint.config
* ESLint flat configuration for Vue, Cypress, and
* Prettier integration.
*/
import js from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
import pluginCypress from "eslint-plugin-cypress";
import skipFormatting from "@vue/eslint-config-prettier";
/** Browser runtime globals used across app and jsdom tests. */
const browserGlobals = {
window: "readonly",
document: "readonly",
navigator: "readonly",
location: "readonly",
localStorage: "readonly",
sessionStorage: "readonly",
console: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
FormData: "readonly",
Blob: "readonly",
URL: "readonly",
atob: "readonly",
btoa: "readonly",
};
/** Node.js globals used in config files. */
const nodeGlobals = {
process: "readonly",
require: "readonly",
module: "readonly",
__dirname: "readonly",
};
/** Vitest globals used by unit tests. */
const vitestGlobals = {
describe: "readonly",
it: "readonly",
test: "readonly",
expect: "readonly",
beforeEach: "readonly",
afterEach: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
vi: "readonly",
};
export default [
{
ignores: [
"node_modules/**",
"dist/**",
"coverage/**",
"cypress/videos/**",
"cypress/screenshots/**",
"excludes/**",
"**/*.ts",
"**/*.d.ts",
],
},
{
files: ["**/*.{js,mjs,cjs,vue}"],
...js.configs.recommended,
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...browserGlobals,
},
},
rules: {
"vue/multi-word-component-names": "error",
},
},
...pluginVue.configs["flat/essential"],
skipFormatting,
{
files: ["tests/**/*.{js,mjs,cjs}"],
languageOptions: {
globals: {
...browserGlobals,
...nodeGlobals,
...vitestGlobals,
},
},
},
{
files: ["cypress/**/*.{js,mjs,cjs}"],
...pluginCypress.configs.recommended,
languageOptions: {
globals: {
...browserGlobals,
...nodeGlobals,
cy: "readonly",
Cypress: "readonly",
},
},
},
{
files: ["*.{js,mjs,cjs}", "**/*.config.{js,mjs,cjs}"],
languageOptions: {
globals: {
...nodeGlobals,
},
},
},
{
files: ["src/**/*.vue", "src/views/**/*.vue", "src/components/**/*.vue"],
rules: {
"vue/multi-word-component-names": "error",
"vue/no-side-effects-in-computed-properties": "error",
"vue/return-in-computed-property": "error",
"vue/no-parsing-error": "error",
"vue/valid-v-else": "error",
"vue/no-deprecated-v-on-native-modifier": "error",
"vue/require-valid-default-prop": "error",
"vue/no-unused-vars": "error",
},
},
];

View File

@@ -1,3 +1,10 @@
<!-- The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31
cindy.chang@dsp.im (Cindy Chang), 2024/7/9
Application entry point HTML template. -->
<!DOCTYPE html>
<html lang="en">
<head>
@@ -8,6 +15,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

16148
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,78 @@
{
"name": "frontend",
"version": "0.2.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"build:e2e": "VITE_MSW=true vite build",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage",
"test:unit": "vitest --environment jsdom --root src/",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
"test:unit": "vitest run",
"test:e2e": "playwright test --config tests/e2e/playwright.config.ts",
"test:e2e:ui": "playwright test --config tests/e2e/playwright.config.ts --ui",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix",
"docs": "typedoc"
},
"dependencies": {
"autoprefixer": "^10.4.13",
"axios": "^1.2.2",
"cytoscape": "^3.23.0",
"@primevue/themes": "^4.5.4",
"@tailwindcss/postcss": "^4.2.1",
"axios": "^1.13.6",
"chart.js": "^4.5.1",
"chartjs-adapter-moment": "^1.0.1",
"chartjs-plugin-datalabels": "^2.2.0",
"cytoscape": "^3.33.1",
"cytoscape-cola": "^2.5.1",
"cytoscape-dagre": "^2.5.0",
"cytoscape-popper": "^2.0.0",
"javascript-color-gradient": "^2.4.4",
"mitt": "^3.0.0",
"moment": "^2.29.4",
"pinia": "^2.0.28",
"postcss": "^8.4.20",
"primeicons": "^6.0.1",
"primevue": "^3.23.0",
"tailwindcss": "^3.2.4",
"cytoscape-fcose": "^2.2.0",
"cytoscape-popper": "^4.0.1",
"decimal.js": "^10.6.0",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
"javascript-color-gradient": "^2.5.0",
"lodash-es": "^4.17.23",
"mitt": "^3.0.1",
"moment": "^2.30.1",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"primeicons": "^7.0.0",
"primevue": "^4.5.4",
"tippy.js": "^6.3.7",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-router": "^4.1.6",
"vue-toast-notification": "^3.0.4",
"vue": "^3.5.29",
"vue-chartjs": "^5.3.3",
"vue-router": "^5.0.3",
"vue-sweetalert2": "^5.0.11",
"vue-toast-notification": "^3.1.3",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.4",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/test-utils": "^2.2.6",
"autoprefixer": "^10.4.13",
"cypress": "^12.0.2",
"eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0",
"html-webpack-plugin": "^5.5.0",
"jsdom": "^20.0.3",
"postcss": "^8.4.20",
"prettier": "^2.7.1",
"sass": "^1.57.1",
"start-server-and-test": "^1.15.2",
"tailwindcss": "^3.2.4",
"vite": "^4.0.0",
"vitest": "^0.25.6"
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"@types/cytoscape": "^3.21.9",
"@types/cytoscape-dagre": "^2.3.4",
"@types/cytoscape-popper": "^2.0.4",
"@types/node": "^25.3.5",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/test-utils": "^2.4.6",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"chartjs-plugin-dragdata": "^2.3.1",
"eslint": "^10.0.2",
"eslint-plugin-vue": "^10.8.0",
"jsdom": "^28.1.0",
"msw": "^2.12.14",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"tailwindcss": "^4.2.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.17",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18",
"vue-eslint-parser": "^10.4.0"
}
}

View File

@@ -1,6 +1,15 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2026/3/6
/**
* @module postcss.config
* PostCSS configuration with Tailwind CSS plugin.
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
};

BIN
public/chartSpace.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

349
public/mockServiceWorker.js Normal file
View File

@@ -0,0 +1,349 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.14'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}

BIN
public/timeFrameSlope.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -2,8 +2,15 @@
<RouterView />
</template>
<style scoped></style>
<script setup lang="ts">
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/**
* @module App Root application component that renders the router view.
*/
<script setup>
import { RouterLink, RouterView } from "vue-router";
import { RouterView } from "vue-router";
</script>

53
src/api/auth.js Normal file
View File

@@ -0,0 +1,53 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// 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";
/**
* Refreshes the access token using the stored refresh token cookie.
*
* Uses plain axios (not apiClient) to avoid interceptor loops. Updates
* both the access token (session cookie) and refresh token (6-month
* expiry) cookies.
*
* @returns {Promise<string>} The new access token.
* @throws {Error} If the refresh request fails.
*/
export async function refreshTokenAndGetNew() {
const api = "/api/oauth/token";
const config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
};
const data = {
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;
if (!newAccessToken || !newRefreshToken) {
throw new Error("Invalid token response structure");
}
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);
return newAccessToken;
}

99
src/api/client.js Normal file
View File

@@ -0,0 +1,99 @@
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module apiClient Centralized axios instance with request/response
* interceptors for authentication token management and automatic
* 401 token refresh with request queuing.
*/
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");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor: handle 401 by attempting token refresh
let isRefreshing = false;
let pendingRequests = [];
/**
* Resolves all pending requests with the new access token.
* @param {string} newToken - The refreshed access token.
*/
function onRefreshSuccess(newToken) {
pendingRequests.forEach((cb) => cb(newToken));
pendingRequests = [];
}
/**
* Rejects all pending requests with the refresh error.
* @param {Error} error - The token refresh error.
*/
function onRefreshFailure(error) {
pendingRequests.forEach((cb) => cb(null, error));
pendingRequests = [];
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Only attempt refresh on 401, and not for auth endpoints or already-retried requests
if (
error.response?.status !== 401 ||
originalRequest._retried ||
originalRequest.url === "/api/oauth/token"
) {
throw error;
}
if (isRefreshing) {
// Queue this request until the refresh completes
return new Promise((resolve, reject) => {
pendingRequests.push((newToken, err) => {
if (err) return reject(err);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
originalRequest._retried = true;
resolve(apiClient(originalRequest));
});
});
}
isRefreshing = true;
originalRequest._retried = true;
try {
// Dynamic import to avoid circular dependency with login store
const { refreshTokenAndGetNew } = await import("@/api/auth.js");
const newToken = await refreshTokenAndGetNew();
isRefreshing = false;
onRefreshSuccess(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
isRefreshing = false;
onRefreshFailure(refreshError);
// Refresh failed: clear auth and redirect to login
deleteCookie("luciaToken");
deleteCookie("luciaRefreshToken");
deleteCookie("isLuciaLoggedIn");
globalThis.location.href = "/login";
throw refreshError;
}
},
);
export default apiClient;

View File

@@ -1,4 +1,13 @@
/* 全域字型 */
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/4/18
cindy.chang@dsp.im (Cindy Chang), 2024/8/16
Base CSS layer with global font, validation, height,
and PrimeVue sidebar overrides. */
/* Global font */
@layer base {
html {
font-family: 'Roboto', sans-serif, system-ui;
@@ -18,3 +27,13 @@
.h-screen-main {
height: calc(100vh - 104px);
}
/* button */
.disable-hover {
@apply pointer-events-none
}
/* Map i panel ; overwrite primevue style */
.p-sidebar .p-sidebar-header {
padding: 8px;
}

10
src/assets/capsule.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,6 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#0099FF"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#BFE5FF"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

6
src/assets/capsule1.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#64748B"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#BFE5FF"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@@ -0,0 +1,7 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#0099FF"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#BFE5FF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#80CCFF"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

7
src/assets/capsule2.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#64748B"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#BFE5FF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#80CCFF"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -0,0 +1,8 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#0099FF"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="9" y="9" width="30" height="30" rx="15" fill="#BFE5FF"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#80CCFF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

8
src/assets/capsule3.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#64748B"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="white"/>
<rect x="9" y="9" width="30" height="30" rx="15" fill="#BFE5FF"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#80CCFF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@@ -0,0 +1,8 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#0099FF"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="#BFE5FF"/>
<rect x="9" y="9" width="30" height="30" rx="15" fill="#80CCFF"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#0099FF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#0073BF"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

8
src/assets/capsule4.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="216" height="48" viewBox="0 0 216 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" fill="white"/>
<rect x="0.5" y="0.5" width="215" height="47" rx="23.5" stroke="#64748B"/>
<rect x="4" y="4" width="40" height="40" rx="20" fill="#BFE5FF"/>
<rect x="9" y="9" width="30" height="30" rx="15" fill="#80CCFF"/>
<rect x="14" y="14" width="20" height="20" rx="10" fill="#0099FF"/>
<rect x="19" y="19" width="10" height="10" rx="5" fill="#0073BF"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -1,3 +1,11 @@
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/2/17
Reusable component styles including loaders, scrollbar,
buttons, and toggle buttons. */
/* loading */
.loader {
width: 64px;
@@ -30,62 +38,29 @@
transform: rotate(360deg)
}
}
/* toggle */
/* <div class="toggle">
<input type="checkbox"/>
<label></label>
</div> */
.toggle {
position: relative;
}
.toggle input[type="checkbox"] {
position: absolute;
left: 0;
top: 0;
z-index: 10;
width: 100%;
height: 100%;
cursor: pointer;
opacity: 0;
}
.toggle label {
position: relative;
display: flex;
align-items: center;
}
.toggle label:before {
content: '';
border: 5px solid #bbb;
height: 35px;
width: 70px;
position: relative;
/* loaderBar */
/* <span class="loaderBar"></span> */
.loaderBar {
width: 80%;
height: 16px;
display: inline-block;
border-radius: 46px;
transition: 0.2s ease-in;
background-color: #0099FF;
border: 1px solid #0099FF;
border-radius: 4px;
background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent);
font-size: 30px;
background-size: 1em 1em;
box-sizing: border-box;
animation: barStripe 1s linear infinite;
}
.toggle label:after {
content: '';
position: absolute;
background: #555;
width: 28px;
height: 28px;
left: 8px;
top: 8px;
border-radius: 50%;
z-index: 2;
box-shadow: 0 0 5px #0002;
transition: 0.2s ease-in;
}
.toggle input[type="checkbox"]:hover + label:after {
box-shadow: 0 2px 15px 0 #0002, 0 3px 8px 0 #0001;
}
.toggle input[type="checkbox"]:checked + label:before {
border-color: #77C2BB;
}
.toggle input[type="checkbox"]:checked + label:after {
background: #009688;
left: 44px;
@keyframes barStripe {
0% {
background-position: 0 0;
}
100% {
background-position: 1em 0;
}
}
/* components */
@@ -100,11 +75,11 @@
}
.scrollbar::-webkit-scrollbar-thumb {
@apply bg-primary rounded-full
@apply bg-neutral-300 rounded-full
}
.scrollbar::-webkit-scrollbar-thumb:hover {
@apply bg-primary
@apply bg-neutral-400
}
}
@@ -119,7 +94,10 @@
@apply px-4 py-2.5
}
.btn-c-primary {
@apply text-neutral-50 bg-primary border border-primary hover:bg-neutral-50 hover:text-primary hover:border hover:border-primary active:border active:ring focus:outline-none focus:border-primary focus:ring
@apply hover:text-neutral-50 hover:bg-primary border border-primary bg-neutral-50 text-primary active:ring focus:outline-none active:ring-primary/50 focus:ring focus:ring-primary/50
}
.btn-cfm-secondary {
@apply border border-cfm-secondary bg-neutral-50 text-cfm-secondary hover:text-neutral-50 hover:bg-cfm-secondary active:ring focus:outline-none active:ring-cfm-secondary/50 focus:ring focus:ring-cfm-secondary/50
}
.btn-disable {
@apply border border-neutral-200 bg-neutral-50 text-neutral-200

View File

@@ -0,0 +1,3 @@
<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.78415 2.03878C9.62269 1.72766 9.36953 1.46508 9.05382 1.28129C8.73812 1.09751 8.37272 1 7.99974 1C7.62675 1 7.26135 1.09751 6.94565 1.28129C6.62994 1.46508 6.37678 1.72766 6.21532 2.03878L0.520353 12.4048C-0.160994 13.6423 0.699988 15.2857 2.30407 15.2857H13.6947C15.2995 15.2857 16.1591 13.643 15.4791 12.4048L9.78415 2.03878ZM7.99974 5.54628C8.18584 5.54628 8.36432 5.61468 8.49591 5.73645C8.6275 5.85822 8.70143 6.02337 8.70143 6.19557V9.44205C8.70143 9.61425 8.6275 9.77941 8.49591 9.90117C8.36432 10.0229 8.18584 10.0913 7.99974 10.0913C7.81363 10.0913 7.63516 10.0229 7.50356 9.90117C7.37197 9.77941 7.29804 9.61425 7.29804 9.44205V6.19557C7.29804 6.02337 7.37197 5.85822 7.50356 5.73645C7.63516 5.61468 7.81363 5.54628 7.99974 5.54628ZM7.99974 11.0653C8.18584 11.0653 8.36432 11.1337 8.49591 11.2555C8.6275 11.3772 8.70143 11.5424 8.70143 11.7146V12.0392C8.70143 12.2114 8.6275 12.3766 8.49591 12.4984C8.36432 12.6201 8.18584 12.6885 7.99974 12.6885C7.81363 12.6885 7.63516 12.6201 7.50356 12.4984C7.37197 12.3766 7.29804 12.2114 7.29804 12.0392V11.7146C7.29804 11.5424 7.37197 11.3772 7.50356 11.2555C7.63516 11.1337 7.81363 11.0653 7.99974 11.0653Z" fill="#FF3366"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="16" height="16" rx="2" fill="white" stroke="#0099FF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="11" viewBox="0 0 14 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 10.4999L0 5.53988L1.59 3.99988L5 7.34988L12.41 -0.00012207L14 1.57988L5 10.4999Z" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="16" height="16" rx="2" fill="white" stroke="#CBD5E1" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="17" viewBox="0 0 22 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.3782 3.89078C21.1637 3.7117 20.9032 3.59652 20.6263 3.5584C20.3495 3.52027 20.0675 3.56073 19.8126 3.67515L15.0688 5.78453L12.3126 0.815776C12.1809 0.583799 11.99 0.390881 11.7594 0.256672C11.5289 0.122463 11.2669 0.0517578 11.0001 0.0517578C10.7333 0.0517578 10.4713 0.122463 10.2408 0.256672C10.0102 0.390881 9.81934 0.583799 9.6876 0.815776L6.93135 5.78453L2.1876 3.67515C1.93214 3.5609 1.64981 3.52039 1.37252 3.5582C1.09524 3.59601 0.834067 3.71064 0.618523 3.88912C0.402979 4.0676 0.241662 4.30282 0.15281 4.56819C0.0639569 4.83356 0.0511114 5.11849 0.115725 5.39078L2.49697 15.5439C2.54251 15.7405 2.62747 15.9257 2.74672 16.0885C2.86597 16.2513 3.01702 16.3881 3.19072 16.4908C3.42589 16.6315 3.69477 16.7061 3.96885 16.7064C4.10208 16.7062 4.23462 16.6872 4.3626 16.6502C8.70306 15.4501 13.2878 15.4501 17.6282 16.6502C18.0245 16.7543 18.446 16.697 18.8001 16.4908C18.9749 16.3894 19.1267 16.253 19.2461 16.09C19.3655 15.927 19.4499 15.7411 19.4938 15.5439L21.8845 5.39078C21.9484 5.11841 21.9348 4.83361 21.8454 4.56855C21.7559 4.30349 21.5941 4.06872 21.3782 3.89078ZM18.0313 15.2064C13.4274 13.9314 8.56346 13.9314 3.95948 15.2064L1.57823 5.05328L6.32197 7.15328C6.66236 7.30802 7.04868 7.32831 7.4034 7.21007C7.75813 7.09182 8.05501 6.8438 8.23447 6.51578L11.0001 1.54703L13.7657 6.51578C13.9452 6.8438 14.2421 7.09182 14.5968 7.21007C14.9515 7.32831 15.3378 7.30802 15.6782 7.15328L20.422 5.05328L18.0313 15.2064ZM14.7501 12.2345C14.7294 12.4194 14.6417 12.5902 14.5034 12.7146C14.3652 12.839 14.1861 12.9084 14.0001 12.9095H13.9251C11.9801 12.7126 10.0201 12.7126 8.0751 12.9095C7.87745 12.9305 7.67955 12.8722 7.52486 12.7474C7.37017 12.6226 7.27135 12.4415 7.2501 12.2439C7.23172 12.0453 7.29222 11.8474 7.41852 11.6931C7.54482 11.5387 7.72679 11.4402 7.9251 11.4189C9.96943 11.2033 12.0308 11.2033 14.0751 11.4189C14.2716 11.4402 14.4521 11.5374 14.5782 11.6897C14.7042 11.842 14.7659 12.0375 14.7501 12.2345Z" fill="#0F172A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5559 5.07692H16.667V4.30769C16.6644 3.69643 16.4178 3.11093 15.9807 2.6787C15.5437 2.24647 14.9517 2.00253 14.3337 2H9.66699C9.04894 2.00253 8.45694 2.24647 8.0199 2.6787C7.58287 3.11093 7.33622 3.69643 7.33366 4.30769V5.07692H3.44477C3.23849 5.07692 3.04066 5.15797 2.8948 5.30223C2.74894 5.44648 2.66699 5.64214 2.66699 5.84615C2.66699 6.05017 2.74894 6.24582 2.8948 6.39008C3.04066 6.53434 3.23849 6.61538 3.44477 6.61538H4.22255V20.4615C4.22255 20.8696 4.38644 21.2609 4.67816 21.5494C4.96988 21.8379 5.36554 22 5.7781 22H18.2225C18.6351 22 19.0308 21.8379 19.3225 21.5494C19.6142 21.2609 19.7781 20.8696 19.7781 20.4615V6.61538H20.5559C20.7622 6.61538 20.96 6.53434 21.1059 6.39008C21.2517 6.24582 21.3337 6.05017 21.3337 5.84615C21.3337 5.64214 21.2517 5.44648 21.1059 5.30223C20.96 5.15797 20.7622 5.07692 20.5559 5.07692ZM8.88921 4.30769C8.88921 4.10368 8.97116 3.90802 9.11702 3.76376C9.26288 3.61951 9.46071 3.53846 9.66699 3.53846H14.3337C14.5399 3.53846 14.7378 3.61951 14.8836 3.76376C15.0295 3.90802 15.1114 4.10368 15.1114 4.30769V5.07692H8.88921V4.30769ZM18.2225 20.4615H5.7781V6.61538H18.2225V20.4615ZM10.4448 10.4615V16.6154C10.4448 16.8194 10.3628 17.0151 10.217 17.1593C10.0711 17.3036 9.87327 17.3846 9.66699 17.3846C9.46071 17.3846 9.26288 17.3036 9.11702 17.1593C8.97116 17.0151 8.88921 16.8194 8.88921 16.6154V10.4615C8.88921 10.2575 8.97116 10.0619 9.11702 9.91761C9.26288 9.77335 9.46071 9.69231 9.66699 9.69231C9.87327 9.69231 10.0711 9.77335 10.217 9.91761C10.3628 10.0619 10.4448 10.2575 10.4448 10.4615ZM15.1114 10.4615V16.6154C15.1114 16.8194 15.0295 17.0151 14.8836 17.1593C14.7378 17.3036 14.5399 17.3846 14.3337 17.3846C14.1274 17.3846 13.9295 17.3036 13.7837 17.1593C13.6378 17.0151 13.5559 16.8194 13.5559 16.6154V10.4615C13.5559 10.2575 13.6378 10.0619 13.7837 9.91761C13.9295 9.77335 14.1274 9.69231 14.3337 9.69231C14.5399 9.69231 14.7378 9.77335 14.8836 9.91761C15.0295 10.0619 15.1114 10.2575 15.1114 10.4615Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5559 5.07692H16.667V4.30769C16.6644 3.69643 16.4178 3.11093 15.9807 2.6787C15.5437 2.24647 14.9517 2.00253 14.3337 2H9.66699C9.04894 2.00253 8.45694 2.24647 8.0199 2.6787C7.58287 3.11093 7.33622 3.69643 7.33366 4.30769V5.07692H3.44477C3.23849 5.07692 3.04066 5.15797 2.8948 5.30223C2.74894 5.44648 2.66699 5.64214 2.66699 5.84615C2.66699 6.05017 2.74894 6.24582 2.8948 6.39008C3.04066 6.53434 3.23849 6.61538 3.44477 6.61538H4.22255V20.4615C4.22255 20.8696 4.38644 21.2609 4.67816 21.5494C4.96988 21.8379 5.36554 22 5.7781 22H18.2225C18.6351 22 19.0308 21.8379 19.3225 21.5494C19.6142 21.2609 19.7781 20.8696 19.7781 20.4615V6.61538H20.5559C20.7622 6.61538 20.96 6.53434 21.1059 6.39008C21.2517 6.24582 21.3337 6.05017 21.3337 5.84615C21.3337 5.64214 21.2517 5.44648 21.1059 5.30223C20.96 5.15797 20.7622 5.07692 20.5559 5.07692ZM8.88921 4.30769C8.88921 4.10368 8.97116 3.90802 9.11702 3.76376C9.26288 3.61951 9.46071 3.53846 9.66699 3.53846H14.3337C14.5399 3.53846 14.7378 3.61951 14.8836 3.76376C15.0295 3.90802 15.1114 4.10368 15.1114 4.30769V5.07692H8.88921V4.30769ZM18.2225 20.4615H5.7781V6.61538H18.2225V20.4615ZM10.4448 10.4615V16.6154C10.4448 16.8194 10.3628 17.0151 10.217 17.1593C10.0711 17.3036 9.87327 17.3846 9.66699 17.3846C9.46071 17.3846 9.26288 17.3036 9.11702 17.1593C8.97116 17.0151 8.88921 16.8194 8.88921 16.6154V10.4615C8.88921 10.2575 8.97116 10.0619 9.11702 9.91761C9.26288 9.77335 9.46071 9.69231 9.66699 9.69231C9.87327 9.69231 10.0711 9.77335 10.217 9.91761C10.3628 10.0619 10.4448 10.2575 10.4448 10.4615ZM15.1114 10.4615V16.6154C15.1114 16.8194 15.0295 17.0151 14.8836 17.1593C14.7378 17.3036 14.5399 17.3846 14.3337 17.3846C14.1274 17.3846 13.9295 17.3036 13.7837 17.1593C13.6378 17.0151 13.5559 16.8194 13.5559 16.6154V10.4615C13.5559 10.2575 13.6378 10.0619 13.7837 9.91761C13.9295 9.77335 14.1274 9.69231 14.3337 9.69231C14.5399 9.69231 14.7378 9.77335 14.8836 9.91761C15.0295 10.0619 15.1114 10.2575 15.1114 10.4615Z" fill="#FF3366"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.75 10.5C18.75 10.6989 18.671 10.8897 18.5303 11.0303C18.3897 11.171 18.1989 11.25 18 11.25H14.25C14.0511 11.25 13.8603 11.171 13.7197 11.0303C13.579 10.8897 13.5 10.6989 13.5 10.5C13.5 10.3011 13.579 10.1103 13.7197 9.96967C13.8603 9.82902 14.0511 9.75 14.25 9.75H18C18.1989 9.75 18.3897 9.82902 18.5303 9.96967C18.671 10.1103 18.75 10.3011 18.75 10.5ZM18 12.75H14.25C14.0511 12.75 13.8603 12.829 13.7197 12.9697C13.579 13.1103 13.5 13.3011 13.5 13.5C13.5 13.6989 13.579 13.8897 13.7197 14.0303C13.8603 14.171 14.0511 14.25 14.25 14.25H18C18.1989 14.25 18.3897 14.171 18.5303 14.0303C18.671 13.8897 18.75 13.6989 18.75 13.5C18.75 13.3011 18.671 13.1103 18.5303 12.9697C18.3897 12.829 18.1989 12.75 18 12.75ZM12.2625 15.5625C12.2878 15.6573 12.2941 15.7562 12.2809 15.8535C12.2677 15.9507 12.2353 16.0444 12.1855 16.129C12.1358 16.2136 12.0698 16.2875 11.9912 16.3463C11.9127 16.4052 11.8233 16.4479 11.7281 16.4719C11.6327 16.4973 11.5332 16.5037 11.4353 16.4906C11.3374 16.4775 11.2431 16.4452 11.1578 16.3955C11.0724 16.3459 10.9977 16.2799 10.9379 16.2013C10.8781 16.1227 10.8344 16.033 10.8094 15.9375C10.6823 15.4576 10.4 15.0333 10.0067 14.7305C9.61326 14.4278 9.13079 14.2636 8.63437 14.2636C8.13796 14.2636 7.65549 14.4278 7.26209 14.7305C6.8687 15.0333 6.58648 15.4576 6.45938 15.9375C6.41615 16.0985 6.32112 16.2409 6.18896 16.3425C6.0568 16.4442 5.89486 16.4995 5.72812 16.5L5.54062 16.4719C5.44547 16.4479 5.35603 16.4052 5.2775 16.3463C5.19898 16.2875 5.13294 16.2136 5.08322 16.129C5.0335 16.0444 5.00109 15.9507 4.98788 15.8535C4.97466 15.7562 4.98091 15.6573 5.00625 15.5625C5.23456 14.676 5.77774 13.9028 6.53437 13.3875C6.10882 12.9704 5.81713 12.436 5.69649 11.8524C5.57586 11.2688 5.63175 10.6626 5.85703 10.1109C6.08232 9.55922 6.46679 9.08714 6.96143 8.75484C7.45608 8.42254 8.03848 8.24507 8.63437 8.24507C9.23027 8.24507 9.81267 8.42254 10.3073 8.75484C10.802 9.08714 11.1864 9.55922 11.4117 10.1109C11.637 10.6626 11.6929 11.2688 11.5723 11.8524C11.4516 12.436 11.1599 12.9704 10.7344 13.3875C11.491 13.9028 12.0342 14.676 12.2625 15.5625ZM8.63437 12.75C8.93105 12.75 9.22106 12.662 9.46773 12.4972C9.7144 12.3324 9.90666 12.0981 10.0202 11.824C10.1337 11.5499 10.1634 11.2483 10.1056 10.9574C10.0477 10.6664 9.90481 10.3991 9.69504 10.1893C9.48526 9.97956 9.21798 9.8367 8.92701 9.77882C8.63604 9.72094 8.33444 9.75065 8.06035 9.86418C7.78626 9.97771 7.55199 10.17 7.38717 10.4166C7.22235 10.6633 7.13438 10.9533 7.13437 11.25C7.13437 11.6478 7.29241 12.0294 7.57372 12.3107C7.85502 12.592 8.23655 12.75 8.63437 12.75ZM21.75 5.25V18.75C21.75 19.1478 21.592 19.5294 21.3107 19.8107C21.0294 20.092 20.6478 20.25 20.25 20.25H3.75C3.35218 20.25 2.97064 20.092 2.68934 19.8107C2.40804 19.5294 2.25 19.1478 2.25 18.75V5.25C2.25 4.85218 2.40804 4.47064 2.68934 4.18934C2.97064 3.90804 3.35218 3.75 3.75 3.75H20.25C20.6478 3.75 21.0294 3.90804 21.3107 4.18934C21.592 4.47064 21.75 4.85218 21.75 5.25ZM20.25 18.75V5.25H3.75V18.75H20.25Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.75 10.5C18.75 10.6989 18.671 10.8897 18.5303 11.0303C18.3897 11.171 18.1989 11.25 18 11.25H14.25C14.0511 11.25 13.8603 11.171 13.7197 11.0303C13.579 10.8897 13.5 10.6989 13.5 10.5C13.5 10.3011 13.579 10.1103 13.7197 9.96967C13.8603 9.82902 14.0511 9.75 14.25 9.75H18C18.1989 9.75 18.3897 9.82902 18.5303 9.96967C18.671 10.1103 18.75 10.3011 18.75 10.5ZM18 12.75H14.25C14.0511 12.75 13.8603 12.829 13.7197 12.9697C13.579 13.1103 13.5 13.3011 13.5 13.5C13.5 13.6989 13.579 13.8897 13.7197 14.0303C13.8603 14.171 14.0511 14.25 14.25 14.25H18C18.1989 14.25 18.3897 14.171 18.5303 14.0303C18.671 13.8897 18.75 13.6989 18.75 13.5C18.75 13.3011 18.671 13.1103 18.5303 12.9697C18.3897 12.829 18.1989 12.75 18 12.75ZM12.2625 15.5625C12.2878 15.6573 12.2941 15.7562 12.2809 15.8535C12.2677 15.9507 12.2353 16.0444 12.1855 16.129C12.1358 16.2136 12.0698 16.2875 11.9912 16.3463C11.9127 16.4052 11.8233 16.4479 11.7281 16.4719C11.6327 16.4973 11.5332 16.5037 11.4353 16.4906C11.3374 16.4775 11.2431 16.4452 11.1578 16.3955C11.0724 16.3459 10.9977 16.2799 10.9379 16.2013C10.8781 16.1227 10.8344 16.033 10.8094 15.9375C10.6823 15.4576 10.4 15.0333 10.0067 14.7305C9.61326 14.4278 9.13079 14.2636 8.63437 14.2636C8.13796 14.2636 7.65549 14.4278 7.26209 14.7305C6.8687 15.0333 6.58648 15.4576 6.45938 15.9375C6.41615 16.0985 6.32112 16.2409 6.18896 16.3425C6.0568 16.4442 5.89486 16.4995 5.72812 16.5L5.54062 16.4719C5.44547 16.4479 5.35603 16.4052 5.2775 16.3463C5.19898 16.2875 5.13294 16.2136 5.08322 16.129C5.0335 16.0444 5.00109 15.9507 4.98788 15.8535C4.97466 15.7562 4.98091 15.6573 5.00625 15.5625C5.23456 14.676 5.77774 13.9028 6.53437 13.3875C6.10882 12.9704 5.81713 12.436 5.69649 11.8524C5.57586 11.2688 5.63175 10.6626 5.85703 10.1109C6.08232 9.55922 6.46679 9.08714 6.96143 8.75484C7.45608 8.42254 8.03848 8.24507 8.63437 8.24507C9.23027 8.24507 9.81267 8.42254 10.3073 8.75484C10.802 9.08714 11.1864 9.55922 11.4117 10.1109C11.637 10.6626 11.6929 11.2688 11.5723 11.8524C11.4516 12.436 11.1599 12.9704 10.7344 13.3875C11.491 13.9028 12.0342 14.676 12.2625 15.5625ZM8.63437 12.75C8.93105 12.75 9.22106 12.662 9.46773 12.4972C9.7144 12.3324 9.90666 12.0981 10.0202 11.824C10.1337 11.5499 10.1634 11.2483 10.1056 10.9574C10.0477 10.6664 9.90481 10.3991 9.69504 10.1893C9.48526 9.97956 9.21798 9.8367 8.92701 9.77882C8.63604 9.72094 8.33444 9.75065 8.06035 9.86418C7.78626 9.97771 7.55199 10.17 7.38717 10.4166C7.22235 10.6633 7.13438 10.9533 7.13437 11.25C7.13437 11.6478 7.29241 12.0294 7.57372 12.3107C7.85502 12.592 8.23655 12.75 8.63437 12.75ZM21.75 5.25V18.75C21.75 19.1478 21.592 19.5294 21.3107 19.8107C21.0294 20.092 20.6478 20.25 20.25 20.25H3.75C3.35218 20.25 2.97064 20.092 2.68934 19.8107C2.40804 19.5294 2.25 19.1478 2.25 18.75V5.25C2.25 4.85218 2.40804 4.47064 2.68934 4.18934C2.97064 3.90804 3.35218 3.75 3.75 3.75H20.25C20.6478 3.75 21.0294 3.90804 21.3107 4.18934C21.592 4.47064 21.75 4.85218 21.75 5.25ZM20.25 18.75V5.25H3.75V18.75H20.25Z" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19H6.4L15.025 10.375L13.625 8.975L5 17.6V19ZM19.3 8.925L15.05 4.725L16.45 3.325C16.8333 2.94167 17.3043 2.75 17.863 2.75C18.421 2.75 18.8917 2.94167 19.275 3.325L20.675 4.725C21.0583 5.10833 21.2583 5.571 21.275 6.113C21.2917 6.65433 21.1083 7.11667 20.725 7.5L19.3 8.925ZM4 21C3.71667 21 3.47933 20.904 3.288 20.712C3.096 20.5207 3 20.2833 3 20V17.175C3 17.0417 3.025 16.9127 3.075 16.788C3.125 16.6627 3.2 16.55 3.3 16.45L13.6 6.15L17.85 10.4L7.55 20.7C7.45 20.8 7.33767 20.875 7.213 20.925C7.08767 20.975 6.95833 21 6.825 21H4ZM14.325 9.675L13.625 8.975L15.025 10.375L14.325 9.675Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19H6.4L15.025 10.375L13.625 8.975L5 17.6V19ZM19.3 8.925L15.05 4.725L16.45 3.325C16.8333 2.94167 17.3043 2.75 17.863 2.75C18.421 2.75 18.8917 2.94167 19.275 3.325L20.675 4.725C21.0583 5.10833 21.2583 5.571 21.275 6.113C21.2917 6.65433 21.1083 7.11667 20.725 7.5L19.3 8.925ZM4 21C3.71667 21 3.47933 20.904 3.288 20.712C3.096 20.5207 3 20.2833 3 20V17.175C3 17.0417 3.025 16.9127 3.075 16.788C3.125 16.6627 3.2 16.55 3.3 16.45L13.6 6.15L17.85 10.4L7.55 20.7C7.45 20.8 7.33767 20.875 7.213 20.925C7.08767 20.975 6.95833 21 6.825 21H4ZM14.325 9.675L13.625 8.975L15.025 10.375L14.325 9.675Z" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5.27L3.28 4L20 20.72L18.73 22L15.65 18.92C14.5 19.3 13.28 19.5 12 19.5C7 19.5 2.73 16.39 1 12C1.69 10.24 2.79 8.69 4.19 7.46L2 5.27ZM12 9C12.7956 9 13.5587 9.31607 14.1213 9.87868C14.6839 10.4413 15 11.2044 15 12C15.0005 12.3406 14.943 12.6787 14.83 13L11 9.17C11.3213 9.05698 11.6594 8.99949 12 9ZM12 4.5C17 4.5 21.27 7.61 23 12C22.1839 14.0732 20.7969 15.8727 19 17.19L17.58 15.76C18.9629 14.8034 20.0782 13.5091 20.82 12C20.0116 10.3499 18.7564 8.95977 17.1973 7.9875C15.6381 7.01524 13.8375 6.49988 12 6.5C10.91 6.5 9.84 6.68 8.84 7L7.3 5.47C8.74 4.85 10.33 4.5 12 4.5ZM3.18 12C3.98844 13.6501 5.24357 15.0402 6.80273 16.0125C8.36189 16.9848 10.1625 17.5001 12 17.5C12.69 17.5 13.37 17.43 14 17.29L11.72 15C11.0242 14.9254 10.3748 14.6149 9.87997 14.12C9.38512 13.6252 9.07458 12.9758 9 12.28L5.6 8.87C4.61 9.72 3.78 10.78 3.18 12Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 969 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 9C12.7956 9 13.5587 9.31607 14.1213 9.87868C14.6839 10.4413 15 11.2044 15 12C15 12.7956 14.6839 13.5587 14.1213 14.1213C13.5587 14.6839 12.7956 15 12 15C11.2044 15 10.4413 14.6839 9.87868 14.1213C9.31607 13.5587 9 12.7956 9 12C9 11.2044 9.31607 10.4413 9.87868 9.87868C10.4413 9.31607 11.2044 9 12 9ZM12 4.5C17 4.5 21.27 7.61 23 12C21.27 16.39 17 19.5 12 19.5C7 19.5 2.73 16.39 1 12C2.73 7.61 7 4.5 12 4.5ZM3.18 12C3.98825 13.6503 5.24331 15.0407 6.80248 16.0133C8.36165 16.9858 10.1624 17.5013 12 17.5013C13.8376 17.5013 15.6383 16.9858 17.1975 16.0133C18.7567 15.0407 20.0117 13.6503 20.82 12C20.0117 10.3497 18.7567 8.95925 17.1975 7.98675C15.6383 7.01424 13.8376 6.49868 12 6.49868C10.1624 6.49868 8.36165 7.01424 6.80248 7.98675C5.24331 8.95925 3.98825 10.3497 3.18 12Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 10.0009C20.0001 8.33719 19.5851 6.69976 18.7926 5.23693C18.0002 3.7741 16.8554 2.5321 15.4618 1.62342C14.0683 0.714739 12.47 0.168101 10.8119 0.0330203C9.15382 -0.10206 7.48821 0.178686 5.96597 0.849826C4.44374 1.52097 3.11298 2.5613 2.09425 3.87657C1.07552 5.19185 0.401001 6.74052 0.131798 8.38229C-0.137405 10.0241 0.00721324 11.7071 0.552553 13.2788C1.09789 14.8506 2.02672 16.2615 3.2549 17.3837L3.37255 17.4817C5.19907 19.104 7.55712 20 10 20C12.4429 20 14.8009 19.104 16.6275 17.4817L16.7451 17.3837C17.7716 16.4474 18.5913 15.3071 19.1518 14.0359C19.7123 12.7646 20.0012 11.3903 20 10.0009ZM1.17647 10.0009C1.1745 8.56033 1.52523 7.1412 2.19802 5.86743C2.87081 4.59365 3.84521 3.50398 5.03612 2.69356C6.22703 1.88315 7.59822 1.37664 9.02997 1.21827C10.4617 1.0599 11.9104 1.25448 13.2497 1.78503C14.5889 2.31558 15.7779 3.16596 16.7127 4.26191C17.6476 5.35785 18.3 6.66602 18.6129 8.07219C18.9258 9.47835 18.8896 10.9397 18.5076 12.3287C18.1257 13.7177 17.4095 14.992 16.4216 16.0404C15.495 14.6071 14.114 13.5264 12.5 12.9716C13.3057 12.435 13.9174 11.6532 14.2445 10.7421C14.5716 9.83088 14.5969 8.83861 14.3166 7.91195C14.0362 6.9853 13.4652 6.17346 12.6878 5.59646C11.9105 5.01945 10.9681 4.70792 10 4.70792C9.03193 4.70792 8.08954 5.01945 7.31218 5.59646C6.53483 6.17346 5.96377 6.9853 5.68343 7.91195C5.40309 8.83861 5.42836 9.83088 5.7555 10.7421C6.08264 11.6532 6.69429 12.435 7.5 12.9716C5.88597 13.5264 4.50496 14.6071 3.57843 16.0404C2.03426 14.409 1.17458 12.2473 1.17647 10.0009ZM6.66667 9.21653C6.66667 8.55722 6.86217 7.91271 7.22844 7.36452C7.59471 6.81632 8.1153 6.38906 8.72439 6.13675C9.33348 5.88444 10.0037 5.81843 10.6503 5.94705C11.2969 6.07568 11.8909 6.39317 12.357 6.85937C12.8232 7.32557 13.1407 7.91955 13.2693 8.56619C13.3979 9.21283 13.3319 9.88309 13.0796 10.4922C12.8273 11.1013 12.4001 11.622 11.8519 11.9883C11.3037 12.3545 10.6593 12.5501 10 12.5501C9.11674 12.5475 8.2704 12.1954 7.64584 11.5708C7.02128 10.9462 6.66926 10.0998 6.66667 9.21653ZM4.45098 16.864C5.02766 15.908 5.84156 15.1171 6.81377 14.5681C7.78598 14.0191 8.88352 13.7307 10 13.7307C11.1165 13.7307 12.214 14.0191 13.1862 14.5681C14.1584 15.1171 14.9723 15.908 15.549 16.864C13.9778 18.1327 12.0194 18.8246 10 18.8246C7.98061 18.8246 6.02219 18.1327 4.45098 16.864Z" fill="#0F172A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 16.0014C32.0001 13.3395 31.3361 10.7196 30.0682 8.37909C28.8003 6.03856 26.9685 4.05135 24.7389 2.59747C22.5092 1.14358 19.9521 0.268961 17.2991 0.0528324C14.6461 -0.163296 11.9811 0.285897 9.54556 1.35972C7.10998 2.43355 4.98077 4.09807 3.3508 6.20252C1.72083 8.30696 0.641601 10.7848 0.210876 13.4117C-0.219849 16.0385 0.0115412 18.7313 0.884085 21.2461C1.75663 23.761 3.24275 26.0184 5.20784 27.8139L5.39608 27.9707C8.31851 30.5664 12.0914 32 16 32C19.9086 32 23.6815 30.5664 26.6039 27.9707L26.7922 27.8139C28.4345 26.3158 29.7461 24.4914 30.6428 22.4574C31.5396 20.4233 32.0019 18.2244 32 16.0014ZM1.88236 16.0014C1.8792 13.6965 2.44036 11.4259 3.51683 9.38788C4.5933 7.34985 6.15233 5.60636 8.05779 4.3097C9.96324 3.01303 12.1572 2.20262 14.4479 1.94922C16.7387 1.69583 19.0567 2.00717 21.1995 2.85605C23.3422 3.70493 25.2446 5.06554 26.7404 6.81905C28.2362 8.57255 29.28 10.6656 29.7806 12.9155C30.2812 15.1654 30.2234 17.5036 29.6122 19.7259C29.0011 21.9483 27.8551 23.9873 26.2745 25.6647C24.7921 23.3713 22.5825 21.6422 20 20.7546C21.2891 19.8959 22.2678 18.6452 22.7912 17.1873C23.3146 15.7294 23.3551 14.1418 22.9065 12.6591C22.458 11.1765 21.5443 9.87753 20.3005 8.95433C19.0567 8.03112 17.5489 7.53267 16 7.53267C14.4511 7.53267 12.9433 8.03112 11.6995 8.95433C10.4557 9.87753 9.54202 11.1765 9.09348 12.6591C8.64494 14.1418 8.68537 15.7294 9.2088 17.1873C9.73222 18.6452 10.7109 19.8959 12 20.7546C9.41754 21.6422 7.20793 23.3713 5.72549 25.6647C3.25482 23.0543 1.87932 19.5957 1.88236 16.0014ZM10.6667 14.7464C10.6667 13.6915 10.9795 12.6603 11.5655 11.7832C12.1515 10.9061 12.9845 10.2225 13.959 9.8188C14.9336 9.41511 16.0059 9.30948 17.0405 9.51528C18.075 9.72108 19.0254 10.2291 19.7712 10.975C20.5171 11.7209 21.0251 12.6713 21.2309 13.7059C21.4366 14.7405 21.331 15.8129 20.9274 16.7875C20.5237 17.7621 19.8401 18.5951 18.963 19.1812C18.086 19.7673 17.0548 20.0801 16 20.0801C14.5868 20.0759 13.2326 19.5127 12.2333 18.5133C11.234 17.514 10.6708 16.1597 10.6667 14.7464ZM7.12157 26.9824C8.04426 25.4527 9.34649 24.1873 10.902 23.309C12.4576 22.4306 14.2136 21.9691 16 21.9691C17.7864 21.9691 19.5424 22.4306 21.098 23.309C22.6535 24.1873 23.9557 25.4527 24.8784 26.9824C22.3645 29.0122 19.231 30.1194 16 30.1194C12.769 30.1194 9.6355 29.0122 7.12157 26.9824Z" fill="#0099FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1386 8.155C38.4927 6.91052 37.4801 5.8602 36.2172 5.12506C34.9544 4.38992 33.4928 3.99988 32.0009 3.99988C30.509 3.99988 29.0474 4.38992 27.7846 5.12506C26.5217 5.8602 25.5091 6.91052 24.8632 8.155L2.08337 49.619C-0.642023 54.5693 2.8019 61.1427 9.21822 61.1427H54.7808C61.1999 61.1427 64.6382 54.5719 61.9184 49.619L39.1386 8.155ZM32.0009 22.185C32.7453 22.185 33.4592 22.4586 33.9856 22.9457C34.512 23.4327 34.8077 24.0933 34.8077 24.7822V37.7681C34.8077 38.4569 34.512 39.1175 33.9856 39.6046C33.4592 40.0916 32.7453 40.3653 32.0009 40.3653C31.2565 40.3653 30.5426 40.0916 30.0162 39.6046C29.4898 39.1175 29.1941 38.4569 29.1941 37.7681V24.7822C29.1941 24.0933 29.4898 23.4327 30.0162 22.9457C30.5426 22.4586 31.2565 22.185 32.0009 22.185ZM32.0009 44.261C32.7453 44.261 33.4592 44.5347 33.9856 45.0217C34.512 45.5088 34.8077 46.1694 34.8077 46.8582V48.1568C34.8077 48.8456 34.512 49.5062 33.9856 49.9933C33.4592 50.4804 32.7453 50.754 32.0009 50.754C31.2565 50.754 30.5426 50.4804 30.0162 49.9933C29.4898 49.5062 29.1941 48.8456 29.1941 48.1568V46.8582C29.1941 46.1694 29.4898 45.5088 30.0162 45.0217C30.5426 44.5347 31.2565 44.261 32.0009 44.261Z" fill="#FF3366"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 13L19 9M19 9L15 5M19 9H5M11 13V14C11 14.7956 10.6839 15.5587 10.1213 16.1213C9.55871 16.6839 8.79565 17 8 17H4C3.20435 17 2.44129 16.6839 1.87868 16.1213C1.31607 15.5587 1 14.7956 1 14V4C1 3.20435 1.31607 2.44129 1.87868 1.87868C2.44129 1.31607 3.20435 1 4 1H8C8.79565 1 9.55871 1.31607 10.1213 1.87868C10.6839 2.44129 11 3.20435 11 4V5" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

4
src/assets/icon-new.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="35" height="16" viewBox="0 0 35 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.11 14.49L30.19 7.87L34.07 1.52C34.1621 1.36868 34.2124 1.1956 34.2157 1.01848C34.2189 0.841351 34.1751 0.666534 34.0886 0.511912C34.0022 0.35729 33.8762 0.228414 33.7236 0.138469C33.5709 0.0485231 33.3972 0.000737145 33.22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585787C0.210714 0.96086 0 1.46957 0 2L0 14C0 14.5304 0.210714 15.0391 0.585786 15.4142C0.960859 15.7893 1.46957 16 2 16H33.25C33.4265 16 33.5999 15.9532 33.7524 15.8645C33.905 15.7758 34.0314 15.6483 34.1188 15.4949C34.2061 15.3415 34.2513 15.1678 34.2498 14.9913C34.2482 14.8148 34.2 14.6418 34.11 14.49ZM10.51 11.18H9.39L6.13 6.84V11.19H5V5H6.13L9.4 9.35V5H10.52L10.51 11.18ZM16.84 6H13.31V7.49H16.51V8.49H13.31V10.1H16.84V11.1H12.18V5H16.83L16.84 6ZM25.13 11.16H24L22.45 6.57L20.9 11.18H19.78L17.78 5H19L20.32 9.43L21.84 5H23.06L24.52 9.43L25.85 5H27.08L25.13 11.16Z" fill="#FFCC44"/>
<path d="M34.11 14.49L30.19 7.87L34.07 1.52C34.1621 1.36868 34.2124 1.1956 34.2157 1.01848C34.2189 0.841351 34.1751 0.666534 34.0886 0.511912C34.0022 0.35729 33.8762 0.228414 33.7236 0.138469C33.5709 0.0485231 33.3972 0.000737145 33.22 0H2C1.46957 0 0.960859 0.210714 0.585786 0.585787C0.210714 0.96086 0 1.46957 0 2L0 14C0 14.5304 0.210714 15.0391 0.585786 15.4142C0.960859 15.7893 1.46957 16 2 16H33.25C33.4265 16 33.5999 15.9532 33.7524 15.8645C33.905 15.7758 34.0314 15.6483 34.1188 15.4949C34.2061 15.3415 34.2513 15.1678 34.2498 14.9913C34.2482 14.8148 34.2 14.6418 34.11 14.49ZM10.51 11.18H9.39L6.13 6.84V11.19H5V5H6.13L9.4 9.35V5H10.52L10.51 11.18ZM16.84 6H13.31V7.49H16.51V8.49H13.31V10.1H16.84V11.1H12.18V5H16.83L16.84 6ZM25.13 11.16H24L22.45 6.57L20.9 11.18H19.78L17.78 5H19L20.32 9.43L21.84 5H23.06L24.52 9.43L25.85 5H27.08L25.13 11.16Z" fill="#FFAA44"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 430 B

3
src/assets/icon-x.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7081 18.2926C19.801 18.3855 19.8747 18.4958 19.9249 18.6172C19.9752 18.7386 20.0011 18.8687 20.0011 19.0001C20.0011 19.1315 19.9752 19.2616 19.9249 19.383C19.8747 19.5044 19.801 19.6147 19.7081 19.7076C19.6151 19.8005 19.5048 19.8742 19.3835 19.9245C19.2621 19.9747 19.132 20.0006 19.0006 20.0006C18.8692 20.0006 18.7391 19.9747 18.6177 19.9245C18.4963 19.8742 18.386 19.8005 18.2931 19.7076L10.0006 11.4138L1.70806 19.7076C1.52042 19.8952 1.26592 20.0006 1.00056 20.0006C0.735192 20.0006 0.480697 19.8952 0.293056 19.7076C0.105415 19.5199 5.23096e-09 19.2654 0 19.0001C-5.23096e-09 18.7347 0.105415 18.4802 0.293056 18.2926L8.58681 10.0001L0.293056 1.70757C0.105415 1.51993 -1.97712e-09 1.26543 0 1.00007C1.97712e-09 0.734704 0.105415 0.480208 0.293056 0.292568C0.480697 0.104927 0.735192 -0.000488279 1.00056 -0.000488281C1.26592 -0.000488283 1.52042 0.104927 1.70806 0.292568L10.0006 8.58632L18.2931 0.292568C18.4807 0.104927 18.7352 -0.000488286 19.0006 -0.000488281C19.2659 -0.000488276 19.5204 0.104927 19.7081 0.292568C19.8957 0.480208 20.0011 0.734704 20.0011 1.00007C20.0011 1.26543 19.8957 1.51993 19.7081 1.70757L11.4143 10.0001L19.7081 18.2926Z" fill="#64748B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,6 +1,16 @@
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/4/18
Layout styles for navbar and heading elements. */
/* Navbar */
nav ul>li {
@apply px-2 py-3.5 duration-300 hover:bg-neutral-900 hover:text-neutral-10 active:bg-neutral-900 active:text-neutral-10;
@apply px-2 py-3.5 duration-300 hover:bg-neutral-900 hover:text-neutral-10 ;
}
nav ul>li.active {
@apply bg-neutral-900 text-neutral-10 duration-300;
}
/* Header */

View File

@@ -1,5 +1,14 @@
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31
cindy.chang@dsp.im (Cindy Chang), 2024/6/18
Main CSS entry point that imports all stylesheet modules. */
@import './tailwind.css';
@import './base.css';
@import './components.css';
@import './layout.css';
@import './vendors.css';
@import './zindex.css';

23
src/assets/radioOff.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg width="40" height="24" viewBox="0 0 40 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3442_4283)">
<rect width="40" height="24" rx="12" fill="#C9CDD4"/>
<g filter="url(#filter0_d_3442_4283)">
<circle cx="12" cy="12" r="10" fill="white"/>
</g>
</g>
<rect x="1" y="1" width="38" height="22" rx="11" stroke="#64748B" stroke-width="2"/>
<defs>
<filter id="filter0_d_3442_4283" x="0" y="2" 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="2"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3442_4283"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3442_4283" result="shape"/>
</filter>
<clipPath id="clip0_3442_4283">
<rect width="40" height="24" rx="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

23
src/assets/radioOn.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg width="40" height="24" viewBox="0 0 40 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3442_4281)">
<rect width="40" height="24" rx="12" fill="#0099FF"/>
<g filter="url(#filter0_d_3442_4281)">
<circle cx="28" cy="12" r="10" fill="white"/>
</g>
</g>
<rect x="1" y="1" width="38" height="22" rx="11" stroke="#0080D5" stroke-width="2"/>
<defs>
<filter id="filter0_d_3442_4281" x="16" y="2" 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="2"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3442_4281"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3442_4281" result="shape"/>
</filter>
<clipPath id="clip0_3442_4281">
<rect width="40" height="24" rx="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,6 +1,84 @@
/* 引入 Google fonts */
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/1/31
imacat.yang@dsp.im (imacat), 2026/3/6
Tailwind CSS theme configuration with custom colors,
font sizes, breakpoints, and animations. */
/* Import Google fonts */
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss" layer(tailwind-base);
@layer tailwind-base, primevue;
@theme {
--color-*: initial;
--breakpoint-*: initial;
--font-size-*: initial;
--color-transparent: transparent;
--color-current: currentColor;
--color-primary: #0099FF;
--color-secondary: #FFAA44;
--color-cfm-primary: #0099FF;
--color-cfm-secondary: #FFAA44;
--color-neutral-10: #ffffff;
--color-neutral-50: #f8fafc;
--color-neutral-100: #f1f5f9;
--color-neutral-200: #e2e8f0;
--color-neutral-300: #cbd5e1;
--color-neutral-400: #94a3b8;
--color-neutral-500: #64748b;
--color-neutral-600: #475569;
--color-neutral-700: #334155;
--color-neutral-800: #1e293b;
--color-neutral-900: #0f172a;
--color-danger: #FF3366;
--container-center: true;
--container-padding: 16px;
--font-size-xs: 12px;
--font-size-xs--line-height: 1;
--font-size-sm: 14px;
--font-size-sm--line-height: 1;
--font-size-base: 16px;
--font-size-base--line-height: 1;
--font-size-lg: 18px;
--font-size-lg--line-height: 1;
--font-size-xl: 20px;
--font-size-xl--line-height: 1;
--font-size-2xl: 24px;
--font-size-2xl--line-height: 1;
--font-size-3xl: 30px;
--font-size-3xl--line-height: 1;
--font-size-4xl: 36px;
--font-size-4xl--line-height: 1;
--breakpoint-2xl: 1536px;
--animate-fadein: fadein 1s ease-in-out;
--animate-fadeout: fadeout 1s ease-in-out;
--animate-edit: edit 0.6s ease;
}
@keyframes fadein {
0% { opacity: 0; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes fadeout {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 0; }
}
@keyframes edit {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(9deg); }
75% { transform: rotate(-9deg); }
}

View File

@@ -1,3 +1,12 @@
/* The Lucia project.
Copyright 2023-2026 DSP, inc. All rights reserved.
Authors:
chiayin.kuo@dsp.im (chiayin), 2023/3/14
cindy.chang@dsp.im (Cindy Chang), 2024/7/26
Third-party vendor style overrides for Google Material
Icons, vue-toast-notification, and PrimeVue components. */
/* import Google font icon */
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
@@ -9,26 +18,95 @@
'GRAD' 0,
'opsz' 40
}
.material-fill {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 40
}
/* vue-toast-notification */
.v-toast {
@apply z-[99999]
}
.v-toast__item {
@apply min-h-[48px] rounded-full
}
.v-toast__item .v-toast__text {
@apply py-3
}
/* Primevue */
/* sidebar */
.p-sidebar-left {
@apply ml-14
}
.p-sidebar-mask {
height: calc(100vh - 104px) !important;
top: 104px !important;
z-index: 20;
}
.p-sidebar {
@apply !shadow-[1px_0px_4px_rgba(0,0,0,0.25)]
}
.p-sidebar-header {
@apply bg-neutral-200 border-b border-neutral-300 !py-2 !justify-between
};
}
.p-sidebar-right .p-sidebar-header {
@apply flex-row-reverse !justify-end text-neutral-500
}
.p-sidebar-right .p-sidebar {
@apply !shadow-[-1px_0px_4px_rgba(0,0,0,0.25)]
}
/* inputswitch */
.p-inputswitch {
@apply !w-11 !h-6
}
.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider {
@apply !bg-primary
}
.p-inputswitch .p-inputswitch-slider:before {
@apply !w-5 !h-5 !left-0.5
}
/* slider */
.p-slider .p-slider-handle {
@apply !h-3.5 !w-3.5 !border !border-primary
}
.p-slider.p-slider-horizontal .p-slider-handle {
@apply !-my-2
}
.p-slider.p-slider-horizontal {
@apply !h-1
}
/* radio */
/* p-radiobutton
p-radiobutton-box
p-radiobutton-icon */
.p-radiobutton {
@apply !align-text-top
}
.p-radiobutton-box {
@apply !bg-neutral-10
}
.p-radiobutton-box .p-highlight:not(.p-disabled):hover {
@apply !bg-neutral-10
}
.p-radiobutton-icon {
@apply !bg-primary
}
/* Dialog */
.p-dialog-header {
@apply !p-0 !px-4 !bg-neutral-100
}
.p-dialog-content {
@apply !p-0
}
/* DataTable */
.p-datatable-resizable > .p-datatable-wrapper {
@apply !overflow-x-visible
}
/* Override inline style overflow-auto so that thead can be positioned */
.p-datatable-wrapper {
overflow: unset !important;
}

11
src/assets/zindex.css Normal file
View File

@@ -0,0 +1,11 @@
/* The Lucia project.
Copyright 2024-2026 DSP, inc. All rights reserved.
Authors:
cindy.chang@dsp.im (Cindy Chang), 2024/6/18
Z-index and positioning rules for the main content area. */
main.w-full {
z-index: 1;
position: absolute; /*if it were static, the acct mgmt menu would be overlapped*/
}

View File

@@ -0,0 +1,188 @@
<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>
</template>
<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 components/AccountMenu/AcctMenu Dropdown account menu
* with links to account management, my account, and logout.
*/
import { computed, onMounted, onBeforeUnmount, 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();
const loginStore = useLoginStore();
const allMapDataStore = useAllMapDataStore();
const conformanceStore = useConformanceStore();
const acctMgmtStore = useAcctMgmtStore();
const { logOut } = loginStore;
const { tempFilterId } = storeToRefs(allMapDataStore);
const { conformanceLogTempCheckId, conformanceFilterTempCheckId } =
storeToRefs(conformanceStore);
const { userData } = storeToRefs(loginStore);
const { isAcctMenuOpen } = storeToRefs(acctMgmtStore);
const loginUserData = ref(null);
const currentViewingUserDetail = computed(
() => acctMgmtStore.currentViewingUser.detail,
);
const isAdmin = ref(false);
/** Fetches user data and determines if the current user is an admin. */
const getIsAdminValue = async () => {
try {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
await acctMgmtStore.getUserDetail(loginUserData.value.username);
isAdmin.value = acctMgmtStore.currentViewingUser.is_admin;
} catch (error) {
console.error("Failed to fetch admin status:", error);
}
};
/** Navigates to the My Account page. */
const onBtnMyAccountClick = async () => {
try {
acctMgmtStore.closeAcctMenu();
await acctMgmtStore.getAllUserAccounts(); // in case we haven't fetched yet
await acctMgmtStore.setCurrentViewingUser(loginUserData.value.username);
await router.push("/my-account");
} catch (error) {
console.error("Failed to navigate to My Account:", error);
}
};
/**
* Closes the menu when clicking outside. Stored as a named
* function so it can be removed in onBeforeUnmount.
* @param {MouseEvent} event - The click event.
*/
const handleDocumentClick = (event) => {
const acctMgmtButton = document.getElementById("acct_mgmt_button");
const acctMgmtMenu = document.getElementById("account_menu");
if (
acctMgmtMenu &&
acctMgmtButton &&
!acctMgmtMenu.contains(event.target) &&
!acctMgmtButton.contains(event.target)
) {
acctMgmtStore.closeAcctMenu();
}
};
/** Registers a click listener to close the menu when clicking outside. */
const clickOtherPlacesThenCloseMenu = () => {
document.addEventListener("click", handleDocumentClick);
};
/** Navigates to the Account Admin page. */
const onBtnAcctMgmtClick = () => {
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
) {
// 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,
);
} else {
logOut();
}
};
// created
loginStore.getUserData();
// mounted
onMounted(async () => {
await getIsAdminValue();
clickOtherPlacesThenCloseMenu();
});
onBeforeUnmount(() => {
document.removeEventListener("click", handleDocumentClick);
});
</script>
<style>
#account_menu {
z-index: 1100;
}
</style>

View File

@@ -0,0 +1,58 @@
<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>
</template>
<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 components/AccountMenu/SearchBar Search input bar for
* filtering accounts, emits search query on click or Enter key.
*/
import { ref } from "vue";
import i18next from "@/i18n/i18n.js";
const emit = defineEmits(["on-search-account-button-click"]);
const inputQuery = ref("");
/**
* Emits the search query when the search icon is clicked.
* @param {Event} event - The click event.
*/
const onSearchClick = (event) => {
event.preventDefault();
emit("on-search-account-button-click", inputQuery.value);
};
/**
* Emits the search query when Enter key is pressed.
* @param {KeyboardEvent} event - The keypress event.
*/
const handleKeyPressOfSearch = (event) => {
if (event.key === "Enter") {
emit("on-search-account-button-click", inputQuery.value);
}
};
</script>

View File

@@ -0,0 +1,45 @@
<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>
</template>
<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 components/Button Outlined button component with
* press-state ring effect.
*/
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
};
</script>

View File

@@ -0,0 +1,40 @@
<template>
<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"
>
<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>
<button class="pr-2">
<IconSearch class="w-6 h-6"></IconSearch>
</button>
</span>
</label>
</form>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Search Search bar component with activity
* search input, settings icon, and search icon button.
*/
import IconSearch from "@/components/icons/IconSearch.vue";
import IconSetting from "@/components/icons/IconSetting.vue";
</script>

View File

@@ -0,0 +1,117 @@
<template>
<div id="header.vue" class="mx-auto px-4 h-14 z-50">
<div class="flex justify-between items-center h-full">
<figure>
<DspLogo />
</figure>
<div
class="flex justify-between items-center relative"
v-show="showMember"
>
<span id="acct_mgmt_button">
<img
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
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"
/>
</span>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Header Application header with DSP logo and
* 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";
const route = useRoute();
const store = useLoginStore();
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 isHeadHovered = ref(false);
const showMember = ref(false);
const toggleIsAcctMenuOpen = () => {
acctMgmtStore.toggleIsAcctMenuOpen();
};
/**
* Handles logout with unsaved-changes confirmation for Map
* and Conformance pages.
*/
function logOutButton() {
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,
);
} else {
logOut();
}
}
onMounted(() => {
if (route.name === "Login" || route.name === "NotFound404") {
showMember.value = false;
} else {
showMember.value = true;
}
});
</script>

View File

@@ -0,0 +1,417 @@
<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"
>
<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"
>
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>
</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)"
v-for="(item, index) in navViewData[navViewName]"
: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"
>
Import
<UploadModal
:visible="uploadModal"
@closeModal="uploadModal = $event"
></UploadModal>
</div>
</div>
<!-- Upload, Performance, Compare have no button actions -->
<div v-else-if="noShowSaveButton"></div>
<!-- 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"
>
Save
</button>
</div>
<AcctMenu v-if="showNavbarBreadcrumb" />
</div>
</div>
</nav>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Navbar Navigation bar with breadcrumb-style
* page tabs, import button for Files, and save button for
* 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 {
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();
const store = useFilesStore();
const allMapDataStore = useAllMapDataStore();
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,
} = storeToRefs(conformanceStore);
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"],
// e.g. DISCOVER: ['MAP', 'CONFORMANCE', 'PERFORMANCE', 'DATA']
DISCOVER: ["MAP", "CONFORMANCE", "PERFORMANCE"],
// e.g. COMPARE: ['PROCESS MAP', 'DASHBOARD']
COMPARE: ["MAP", "PERFORMANCE"],
"ACCOUNT MANAGEMENT": [],
"MY ACCOUNT": [],
};
const navViewName = ref("FILES");
const uploadModal = ref(false);
const disabledSave = computed(() => {
switch (route.name) {
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
);
default:
return true;
}
});
const showIcon = computed(() => {
return !["FILES", "UPLOAD"].includes(navViewName.value);
});
const noShowSaveButton = computed(() => {
return (
navViewName.value === "UPLOAD" ||
navViewName.value === "COMPARE" ||
navViewName.value === "ACCOUNT MANAGEMENT" ||
activePage.value === "PERFORMANCE"
);
});
watch(
() => route,
() => {
getNavViewName();
},
{ deep: true },
);
/**
* Handles navigation when a navbar tab is clicked.
* @param {Event} event - The click event from the nav item.
*/
function onNavItemBtnClick(event) {
let type;
let fileId;
let isCheckPage;
const navItemCandidate = event.target.innerText;
setPendingActivePage(navItemCandidate);
switch (navViewName.value) {
case "FILES":
store.filesTag = navItemCandidate;
break;
case "DISCOVER": {
type = route.params.type;
fileId = route.params.fileId;
isCheckPage = route.name.includes("Check");
const discoverRoutes = {
MAP: "Map",
CONFORMANCE: "Conformance",
PERFORMANCE: "Performance",
};
const baseName = discoverRoutes[navItemCandidate];
if (baseName) {
const routeName = isCheckPage ? `Check${baseName}` : baseName;
router.push({
name: routeName,
params: { type: type, fileId: fileId },
});
}
break;
}
case "COMPARE":
switch (navItemCandidate) {
case "MAP":
router.push({
name: "MapCompare",
params: mapCompareStore.routeParam,
});
break;
case "PERFORMANCE":
router.push({
name: "CompareDashboard",
params: mapCompareStore.routeParam,
});
break;
default:
break;
}
}
}
/**
* Determines the navbar view name and active page from the current route.
* @returns {string} The navigation item name to highlight.
*/
function getNavViewName() {
const name = route.name;
let valueToSet;
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";
switch (navViewName.value) {
case "FILES":
valueToSet = activePage.value;
break;
case "DISCOVER":
switch (name) {
case "Map":
case "CheckMap":
valueToSet = "MAP";
break;
case "Conformance":
case "CheckConformance":
valueToSet = "CONFORMANCE";
break;
case "Performance":
case "CheckPerformance":
valueToSet = "PERFORMANCE";
break;
}
break;
case "COMPARE":
switch (name) {
case "MapCompare":
valueToSet = "MAP";
break;
case "CompareDashboard":
valueToSet = "PERFORMANCE";
break;
default:
break;
}
break;
}
// Frontend is not sure which button will the user press on the modal,
// 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
setPendingActivePage(valueToSet);
}
return valueToSet;
}
/** Opens the save modal for Map or Conformance pages. */
async function saveModal() {
// Help determine MAP/CONFORMANCE save with "submit" or "cancel".
// Notify Map to close the Sidebar.
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;
}
}
/** Sets nav item button background color when the active page is empty. */
function handleNavItemBtn() {
if (activePageComputedByRoute.value === "") {
setActivePageComputedByRoute(route.matched[route.matched.length - 1].name);
}
}
/** Saves or creates a filter for the Map page. */
async function handleMapSave() {
if (createFilterId.value) {
await allMapDataStore.updateFilter();
if (isUpdateFilter.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`);
}
}
}
/** Saves a filter from the Check Map page. */
async function handleCheckMapSave() {
const isSaved = await saveFilter(allMapDataStore.addFilterId);
if (isSaved) {
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);
}
} 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`,
);
}
}
}
}
onMounted(() => {
handleNavItemBtn();
if (route.params.type === "filter") {
createFilterId.value = route.params.fileId;
}
showNavbarBreadcrumb.value = route.matched[0].name !== "AuthContainer";
getNavViewName();
});
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button {
appearance: none;
}
</style>

View File

@@ -0,0 +1,47 @@
<!-- 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>
</template>
<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 components/ButtonFilled Filled button component with
* solid background and press-state ring effect.
*/
import { ref } from "vue";
defineProps({
buttonText: {
type: String,
required: false,
},
});
const isPressed = ref(false);
const onMousedown = () => {
isPressed.value = true;
};
const onMouseup = () => {
isPressed.value = false;
};
</script>

View File

@@ -0,0 +1,441 @@
<template>
<Drawer
: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>
<!-- header: summary -->
<div v-if="primaryStatData && secondaryStatData" class="flex justify-start items-start">
<!-- 001 -->
<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>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-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>
</div>
<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>
</div>
<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>
</div>
<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>
</div>
<span
class="block text-primary text-2xl text-right font-medium basis-28"
>{{ primaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
<!-- Log Timeframe -->
<div class="pt-1 pb-4 border-b border-neutral-300">
<p class="h2">Log Timeframe</p>
<div class="space-y-2 text-sm text-center">
<span class="block">{{ primaryStatData.started_at }}&nbsp;</span>
<span class="block">~</span>
<span class="block">&nbsp;{{ primaryStatData.completed_at }}</span>
</div>
</div>
<!-- Case Duration -->
<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>
</ul>
</div>
</section>
<!-- 002 -->
<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>
</div>
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-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>
</div>
<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>
</div>
<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>
</div>
<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>
</div>
<span
class="block text-secondary text-2xl text-right font-medium basis-28"
>{{ secondaryStatData.tasks.ratio }}%</span
>
</div>
</li>
</ul>
<!-- Log Timeframe -->
<div class="pt-1 pb-4 border-b border-neutral-300">
<p class="h2">Log Timeframe</p>
<div class="space-y-2 text-sm text-center">
<span class="block">{{ secondaryStatData.started_at }}&nbsp;</span>
<span class="block">~</span>
<span class="block"
>&nbsp;{{ secondaryStatData.completed_at }}</span
>
</div>
</div>
<!-- Case Duration -->
<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>
</ul>
</div>
</section>
</div>
</Drawer>
</template>
<script setup>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Compare/SidebarStates Summary sidebar for
* the Compare view showing side-by-side statistics (cases,
* 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";
const props = defineProps({
sidebarState: {
type: Boolean,
require: false,
},
});
const route = useRoute();
const compareStore = useCompareStore();
const primaryValueCases = ref(0);
const primaryValueTraces = ref(0);
const primaryValueTaskInstances = ref(0);
const primaryValueTasks = ref(0);
const secondaryValueCases = ref(0);
const secondaryValueTraces = ref(0);
const secondaryValueTaskInstances = ref(0);
const secondaryValueTasks = ref(0);
const primaryStatData = ref(null);
const secondaryStatData = ref(null);
/**
* Converts a ratio (01) to a percentage number (0100), capped at 100.
* @param {number} val - The ratio value to convert.
* @returns {number} The percentage value.
*/
const getPercentLabel = (val) => {
if (Number((val * 100).toFixed(1)) >= 100) return 100;
else return Number.parseFloat((val * 100).toFixed(1));
};
/**
* Transforms raw API stats into display-ready stat data.
* @param {Object} data - The raw stats from the API.
* @param {string} fileName - The file name to display.
* @returns {Object} The formatted stat data object.
*/
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),
},
traces: {
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),
},
tasks: {
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"),
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. */
const show = () => {
if (!primaryStatData.value || !secondaryStatData.value) return;
primaryValueCases.value = primaryStatData.value.cases.ratio;
primaryValueTraces.value = primaryStatData.value.traces.ratio;
primaryValueTaskInstances.value = primaryStatData.value.task_instances.ratio;
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;
secondaryValueTasks.value = secondaryStatData.value.tasks.ratio;
};
/** Resets all progress bar values to zero when the sidebar is hidden. */
const hide = () => {
primaryValueCases.value = 0;
primaryValueTraces.value = 0;
primaryValueTaskInstances.value = 0;
primaryValueTasks.value = 0;
secondaryValueCases.value = 0;
secondaryValueTraces.value = 0;
secondaryValueTaskInstances.value = 0;
secondaryValueTasks.value = 0;
};
onMounted(async () => {
const routeParams = route.params;
const primaryType = routeParams.primaryType;
const secondaryType = routeParams.secondaryType;
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const primaryData = await compareStore.getStateData(primaryType, primaryId);
const secondaryData = await compareStore.getStateData(
secondaryType,
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);
});
</script>
<style scoped>
:deep(.p-progressbar .p-progressbar-value) {
background-color: var(--bg-color);
}
.progressbar-primary {
--bg-color: #0099ff;
}
.progressbar-secondary {
--bg-color: #ffaa44;
}
</style>

View File

@@ -0,0 +1,995 @@
<template>
<section
class="p-4 mr-0.5 space-y-2 h-full w-[calc(100vw_-_316px)] overflow-y-auto scrollbar float-right"
>
<div
v-show="isCoverPlate"
class="w-[calc(100vw_-_300px)] h-screen-main fixed bottom-0 right-0 bg-gradient-to-tr from-neutral-500/50 to-neutral-900/50 z-[1]"
></div>
<!-- title -->
<p class="text-base leading-10 font-bold">
Conformance Checking Results ({{ data.total }})
</p>
<!-- total group -->
<ul class="text-neutral-10 text-sm flex gap-2 py-2">
<li class="bg-cfm-primary rounded-full px-4 py-1 space-x-2">
<span class="material-symbols-outlined !text-base align-middle mr-2"
>check_circle</span
>Conforming<span>{{ data.counts.conforming }}</span>
</li>
<li class="bg-cfm-secondary rounded-full px-4 py-1 space-x-2">
<span class="material-symbols-outlined !text-base align-middle mr-2"
>cancel</span
>Not Conforming<span>{{ data.counts.not_conforming }}</span>
</li>
<li
class="bg-neutral-700 rounded-full px-4 py-1 space-x-2"
v-show="data.counts.not_applicable != 0"
>
<iconNA class="inline-block mr-1"></iconNA>Not Applicable<span>{{
data.counts.not_applicable
}}</span>
</li>
</ul>
<!-- chart -->
<div class="flex gap-4">
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<div class="p-2 flex justify-between items-center">
<div>
<span class="block text-sm font-bold mb-2"
>Conformance Rate<span
class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer"
v-tooltip.bottom="tooltip.rate"
>info</span
></span
>
<small class="text-neutral-700 font-normal block"
>{{ data.charts.rate.xMin }} ~ {{ data.charts.rate.xMax }}</small
>
</div>
<span class="text-2xl font-bold">{{ data.charts.rate.rate }}%</span>
</div>
<Chart
type="line"
:data="rateChartData"
:options="rateChartOptions"
class="w-[99%]"
/>
</div>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<div class="p-2 flex justify-between items-center">
<div>
<span class="block text-sm font-bold mb-2"
>Cases<span
class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer"
v-tooltip.bottom="tooltip.case"
>info</span
></span
>
<small class="text-neutral-700 font-normal block"
>{{ data.charts.cases.xMin }} ~
{{ data.charts.cases.xMax }}</small
>
</div>
<span class="text-2xl font-bold"
><span class="text-cfm-primary">{{
data.charts.cases.conforming
}}</span
>&nbsp;/&nbsp;{{ data.charts.cases.total }}</span
>
</div>
<Chart
type="bar"
:data="casesChartData"
:options="casesChartOptions"
class="w-[99%]"
/>
</div>
</div>
<!-- effect -->
<section>
<p class="h2 text-base">Effect</p>
<div class="flex gap-4 w-full">
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="h2 pl-2 mb-2">Throughput Time</p>
<div v-if="data.effect.time !== null">
<p
class="pl-2 space-x-2"
v-if="data.effect.time.not_conforming === null"
>
<span
>All cases are conforming to set rules. Average throughput time
is</span
>
<span class="text-cfm-primary text-2xl font-medium">{{
data.effect.time.conforming
}}</span>
<span>days.</span>
</p>
<p
class="pl-2 space-x-2"
v-else-if="data.effect.time.conforming === null"
>
<span
>None of the cases is conforming to set rules. Average
throughput time is</span
>
<span class="text-cfm-secondary text-2xl font-medium">{{
data.effect.time.not_conforming
}}</span>
<span>days.</span>
</p>
<p class="pl-2 space-x-2 max-w-full" v-else>
<span
class="text-cfm-primary text-2xl font-medium inline-block"
>{{ data.effect.time.conforming }}</span
>
<span>vs</span>
<span
class="text-cfm-secondary text-2xl font-medium inline-block"
>{{ data.effect.time.not_conforming }}</span
>
<span>days,</span>
<span class="text-2xl font-medium inline-block">{{
data.effect.time.difference
}}</span>
<span>days of difference.</span>
</p>
</div>
</div>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-1/2">
<p class="h2 pl-2 mb-2">Activities per Case</p>
<div v-if="data.effect.tasks !== null">
<p
class="pl-2 space-x-2"
v-if="data.effect.tasks.not_conforming === null"
>
<span
>All cases are conforming to set rules. Average activities in
per cases is</span
>
<span class="text-cfm-primary text-2xl font-medium">{{
data.effect.tasks.conforming
}}</span>
.
</p>
<p
class="pl-2 space-x-2"
v-else-if="data.effect.tasks.conforming === null"
>
<span
>None of the cases is conforming to set rules. Average
activities in per cases is</span
>
<span class="text-cfm-secondary text-2xl font-medium">{{
data.effect.tasks.not_conforming
}}</span>
.
</p>
<p class="pl-2 space-x-2 max-w-full" v-else>
<span
class="text-cfm-primary text-2xl font-medium inline-block"
>{{ data.effect.tasks.conforming }}</span
>
<span>vs</span>
<span
class="text-cfm-secondary text-2xl font-medium inline-block"
>{{ data.effect.tasks.not_conforming }}</span
>
<span>activities,</span>
<span class="text-2xl font-medium inline-block">{{
data.effect.tasks.difference
}}</span>
<span>activities of difference.</span>
</p>
</div>
</div>
</div>
</section>
<!-- Loop group -->
<section>
<div v-if="data.loops === null"></div>
<div v-else>
<p class="h2 text-base">Loop List</p>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-full">
<p class="h2 pl-2 mb-2">Short Loop(s)</p>
<table class="text-sm min-w-full table-fixed">
<caption class="hidden">
Loop List
</caption>
<thead class="hidden">
<tr>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in data.loops" :key="key">
<td class="p-2 pl-6 truncate max-w-0 w-1/3">
<span
class="material-symbols-outlined disc !text-sm align-middle mr-1"
>fiber_manual_record</span
>{{ trace.label }}
</td>
<td class="p-2 min-w-[96px] w-2/5">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
<td class="p-2 text-right truncate">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
<td class="p-2 text-center">
<div
class="btn btn-sm btn-c-primary cursor-pointer"
@click="openLoopMore(trace.no)"
>
More
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Issues group -->
<section>
<div v-if="data.issues === 'reset'">
<p class="h2 text-base">Non-conformance Issues</p>
<div class="border rounded border-neutral-300 p-2 bg-neutral-10 w-full">
<p class="h2 pl-2 mb-2">Issue List</p>
</div>
</div>
<div v-else>
<div>
<div
v-if="
(data.issues?.length === 0 || data.issues === null) &&
data.timeTrend.chart === null
"
></div>
<p v-else class="h2 text-base">Non-conformance Issues</p>
</div>
<div
class="flex w-full"
:class="
data.issues === null || data.issues?.length === 0 ? '' : 'gap-4'
"
>
<!-- Issues chart -->
<div
v-if="data.timeTrend.chart !== null"
class="border rounded border-neutral-300 p-2 bg-neutral-10"
:class="
data.issues === null || data.issues?.length === 0
? 'w-full'
: 'w-1/2'
"
>
<p class="h2 p-2 flex justify-between items-center">
<span
>Time Trend<span
class="material-symbols-outlined !text-sm align-middle ml-2"
v-tooltip.bottom="tooltip.timeTrend"
>info</span
></span
>
<span class="text-2xl"
><span class="text-cfm-secondary">{{
data.timeTrend.not_conforming
}}</span
>&nbsp;/&nbsp;{{ data.timeTrend.total }}</span
>
</p>
<Chart
type="line"
:data="timeChartData"
:options="timeChartOptions"
class="w-[99%]"
/>
</div>
<!-- Issues list -->
<div v-if="data.issues === null || data.issues?.length === 0"></div>
<div
v-else
class="border rounded border-neutral-300 p-2 bg-neutral-10"
:class="data.timeTrend.chart !== null ? 'w-1/2' : 'w-full'"
>
<p class="h2 pl-2 mb-2">
Issue List<span
class="material-symbols-outlined !text-sm align-middle ml-2 cursor-pointer"
v-tooltip.bottom="tooltip.issueList"
>info</span
>
</p>
<table class="text-sm min-w-full table-fixed">
<caption class="hidden">
Issues List
</caption>
<thead class="hidden">
<tr>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
<th class="w-1/5 px-4 py-2 hidden"></th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in data.issues" :key="key">
<td class="p-2 pl-6 truncate max-w-0 w-1/3">
<span
class="material-symbols-outlined disc !text-sm align-middle mr-1"
>fiber_manual_record</span
>{{ trace.label }}
</td>
<td class="p-2 min-w-[96px] w-2/5">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div
class="h-full bg-cfm-secondary"
:style="trace.value"
></div>
</div>
</td>
<td class="p-2 text-right truncate">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
<td class="p-2 text-center">
<div
class="btn btn-sm btn-cfm-secondary cursor-pointer"
@click="openMore(trace.no)"
>
More
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<MoreModal
:listModal="issuesModal"
@closeModal="issuesModal = $event"
:listTraces="issueTraces"
:taskSeq="taskSeq"
:cases="cases"
:listNo="issuesNo"
:traceId="traceId"
:firstCases="firstCases"
:category="'issue'"
></MoreModal>
<MoreModal
:listModal="loopModal"
@closeModal="loopModal = $event"
:listTraces="loopTraces"
:taskSeq="loopTaskSeq"
:cases="loopCases"
:listNo="loopNo"
:traceId="looptraceId"
:firstCases="loopFirstCases"
:category="'loop'"
></MoreModal>
</section>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceResults
* Conformance checking results panel displaying rule
* check outcomes in a data table with status indicators.
*/
import { ref, watch, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import iconNA from "@/components/icons/IconNA.vue";
import MoreModal from "./MoreModal.vue";
import getNumberLabel from "@/module/numberLabel.js";
import {
setLineChartData,
setBarChartData,
timeRange,
yTimeRange,
getXIndex,
formatTime,
formatMaxTwo,
} from "@/module/setChartData.js";
import shortScaleNumber from "@/module/shortScaleNumber.js";
import getMoment from "moment";
import emitter from "@/utils/emitter";
const conformanceStore = useConformanceStore();
const {
conformanceTempReportData,
issueTraces,
taskSeq,
cases,
loopTraces,
loopTaskSeq,
loopCases,
} = storeToRefs(conformanceStore);
const data = ref({
total: "--",
counts: {
conforming: "--",
not_conforming: "--",
not_applicable: 0,
},
charts: {
rate: {
rate: "--",
chart: {},
},
cases: {
conforming: "--",
total: "--",
chart: {},
},
fitness: "--",
},
effect: {
time: null,
tasks: null,
},
loops: null,
issues: "reset",
timeTrend: {
not_conforming: "--",
total: "--",
chart: {},
},
});
const isCoverPlate = ref(false);
const issuesModal = ref(false);
const loopModal = ref(false);
const rateChartData = ref(null);
const rateChartOptions = ref(null);
const casesChartData = ref(null);
const casesChartOptions = ref(null);
const timeChartData = ref(null);
const timeChartOptions = ref(null);
const issuesNo = ref(null);
const traceId = ref(null);
const firstCases = ref(null);
const loopNo = ref(null);
const looptraceId = ref(null);
const loopFirstCases = ref(null);
const selectDurationTime = ref(null);
const tooltip = ref({
rate: {
value: "= Conforming / (Conforming + Not Conforming) * 100%",
class: "!max-w-[36rem] !text-[10px] !opacity-90",
},
case: {
value: "= Conforming / (Conforming + Not Conforming)",
class: "!max-w-[36rem] !text-[10px] !opacity-90",
},
timeTrend: {
value: "=Not Conforming / (total Conforming+total Not Conforming)",
class: "!max-w-[36rem] !text-[10px] !opacity-90",
},
issueList: {
value:
"Percentage of Issue Type (%) = Cases of Issue Type / Total Cases of All Issue Types.",
class: "!max-w-[36rem] !text-[10px] !opacity-90",
},
});
/**
* set progress bar width
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
const progressWidth = (value) => {
return `width:${value}%;`;
};
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
const getPercentLabel = (val) => {
if (Number((val * 100).toFixed(1)) >= 100) return 100;
else return Number.parseFloat((val * 100).toFixed(1));
};
/**
* Convert seconds to days
* @param {number} sec - The number of seconds.
* @returns {number} day
*/
const convertSecToDay = (sec) => {
return sec / 86400;
};
/**
* Open Issues Modal.
* @param {number} no - The trace number.
*/
const openMore = async (no) => {
// Use async/await to prevent errors caused by asynchronous data not being available yet
issuesNo.value = no;
await conformanceStore.getConformanceIssue(no);
if (issueTraces.value.length === 0) return;
traceId.value = issueTraces.value[0].id;
firstCases.value = await conformanceStore.getConformanceTraceDetail(
no,
issueTraces.value[0].id,
0,
);
issuesModal.value = true;
};
/**
* Open Loop Modal.
* @param {number} no - The trace number.
*/
const openLoopMore = async (no) => {
// Use async/await to prevent errors caused by asynchronous data not being available yet
loopNo.value = no;
await conformanceStore.getConformanceLoop(no);
if (loopTraces.value.length === 0) return;
looptraceId.value = loopTraces.value[0].id;
loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail(
no,
loopTraces.value[0].id,
0,
);
loopModal.value = true;
};
/**
* set conformance report data
* @param {object} data - The report data received from the backend.
*/
const setConformanceTempReportData = (newData) => {
const total = getNumberLabel(
Object.values(newData.counts).reduce((acc, val) => acc + val, 0),
);
const sum = newData.counts.conforming + newData.counts.not_conforming;
const rate = sum === 0 ? "0.0" : ((newData.counts.conforming / sum) * 100).toFixed(1);
const isNullTime = (value) =>
value === null ? null : getNumberLabel((value / 86400).toFixed(1));
const isNullCase = (value) =>
value === null ? null : getNumberLabel(value.toFixed(1));
const setLoopData = (value) => {
if (newData.counts.conforming === 0) return [];
return value.map((item) => {
return {
no: item.no,
label: item.description,
value: `width:${getPercentLabel(item.count / newData.counts.conforming)}%;`,
count: getNumberLabel(item.count),
ratio: getPercentLabel(item.count / newData.counts.conforming),
};
});
};
const setIssueData = (value) => {
if (newData.counts.not_conforming === 0) return [];
return value.map((item) => {
return {
no: item.no,
label: item.description,
value: `width:${getPercentLabel(item.count / newData.counts.not_conforming)}%;`,
count: getNumberLabel(item.count),
ratio: getPercentLabel(item.count / newData.counts.not_conforming),
};
});
};
const isNullLoops = (value) => (value === null ? null : setLoopData(value));
const isNullIsssue = (value) => (value === null ? null : setIssueData(value));
const result = {
total: `Total ${total}`,
counts: {
conforming: getNumberLabel(newData.counts.conforming),
not_conforming: getNumberLabel(newData.counts.not_conforming),
not_applicable: getNumberLabel(newData.counts.not_applicable),
},
charts: {
rate: {
rate: rate,
data: setLineChartData(
newData.charts.rate.data,
newData.charts.rate.x_axis.max,
newData.charts.rate.x_axis.min,
true,
),
xMax: getMoment(newData.charts.rate.x_axis.max).format("YYYY/M/D"),
xMin: getMoment(newData.charts.rate.x_axis.min).format("YYYY/M/D"),
},
cases: {
conforming: getNumberLabel(newData.counts.conforming),
total: getNumberLabel(sum),
data: {
conforming: setBarChartData(
newData.charts.cases.data
.filter((item) => item.label === "conforming")
.map((item) => item.data)[0],
),
not_conforming: setBarChartData(
newData.charts.cases.data
.filter((item) => item.label === "not-conforming")
.map((item) => item.data)[0],
),
},
xMax: getMoment(newData.charts.cases.x_axis.max).format("YYYY/M/D"),
xMin: getMoment(newData.charts.cases.x_axis.min).format("YYYY/M/D"),
},
fitness: getNumberLabel(newData.charts.fitness),
},
effect: {
time: {
conforming: isNullTime(newData.effect.time.conforming),
not_conforming: isNullTime(newData.effect.time.not_conforming),
difference: (
isNullTime(newData.effect.time.conforming) -
isNullTime(newData.effect.time.not_conforming)
).toFixed(1),
},
tasks: {
conforming: isNullCase(newData.effect.tasks.conforming),
not_conforming: isNullCase(newData.effect.tasks.not_conforming),
difference: (
isNullCase(newData.effect.tasks.conforming) -
isNullCase(newData.effect.tasks.not_conforming)
).toFixed(1),
},
},
loops: isNullLoops(newData.loops),
issues: isNullIsssue(newData.issues),
timeTrend: {
not_conforming: getNumberLabel(newData.counts.not_conforming),
total: getNumberLabel(sum),
chart: null,
xMax: null,
xMin: null,
yMax: null,
yMin: null,
},
};
if (newData.charts.time) {
result.timeTrend.chart = setLineChartData(
newData.charts.time.data,
newData.charts.time.x_axis.max,
newData.charts.time.x_axis.min,
false,
newData.charts.time.y_axis.max,
newData.charts.time.y_axis.min,
);
result.timeTrend.xMax = newData.charts.time.x_axis.max;
result.timeTrend.xMin = newData.charts.time.x_axis.min;
result.timeTrend.yMax = newData.charts.time.y_axis.max;
result.timeTrend.yMin = newData.charts.time.y_axis.min;
}
setRateChartData(result.charts.rate.data); // Build the Rate Chart.js chart
setCasesChartData(
result.charts.cases.data.conforming,
result.charts.cases.data.not_conforming,
newData.charts.cases.x_axis.max,
newData.charts.cases.x_axis.min,
); // Build the Cases Chart.js chart
if (newData.charts.time)
setTimeChartData(
result.timeTrend.chart,
result.timeTrend.xMax,
result.timeTrend.xMin,
result.timeTrend.yMax,
result.timeTrend.yMin,
); // Build the Time Chart.js chart
return result;
};
/**
* set Rate Chart Data
* @param {object} data new rate chart data
*/
const setRateChartData = (chartData) => {
rateChartData.value = {
labels: [],
datasets: [
{
label: "Rate",
data: chartData,
fill: false,
pointRadius: 0, // Hide data points
pointHoverRadius: 0, // Hide data points on hover
tension: 0.4,
borderColor: "#0099FF",
x: "x",
y: "y",
},
],
};
rateChartOptions.value = {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 0.6,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
legend: false, // Hide legend
tooltip: {
enabled: false, // Hide tooltip
},
},
scales: {
x: {
type: "time",
ticks: {
display: false,
},
grid: {
display: false, // Hide x-axis grid lines
},
border: {
color: "#334155",
},
},
y: {
beginAtZero: true, // Scale includes 0
suggestedMin: 0,
suggestedMax: 1,
ticks: {
// Set tick intervals
includeBounds: true,
color: "#334155",
align: "inner",
callback: function (value, index, values) {
if (value === 0 || value === 1) {
return `${value * 100}%`;
}
},
},
grid: {
display: false, // Hide y-axis grid lines
},
border: {
color: "#334155",
},
},
},
};
};
/**
* set Cases Chart Data
* @param {array} conformingData new cases chart conforming data
* @param {array} notConformingData new cases chart not conforming data
* @param {number} xMax new cases chart xMax
* @param {number} xMin new cases chart xMin
*/
const setCasesChartData = (conformingData, notConformingData, xMax, xMin) => {
casesChartData.value = {
datasets: [
{
type: "bar",
label: "Conforming",
data: conformingData,
backgroundColor: "#0099FF",
},
{
type: "bar",
label: "Not Conforming",
data: notConformingData,
backgroundColor: "#FFAA44",
},
],
};
casesChartOptions.value = {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
tooltips: {
mode: "index",
intersect: false,
},
legend: false, // Hide legend
},
scales: {
x: {
stacked: true,
ticks: {
display: false,
},
grid: {
display: false, // Hide x-axis grid lines
},
border: {
color: "#334155",
},
},
y: {
stacked: true,
beginAtZero: true, // Scale includes 0
ticks: {
color: "#334155",
align: "inner",
callback: function (value, index, values) {
if (index === 0 || index === values.length - 1) {
return shortScaleNumber(value);
}
},
},
grid: {
// display: false, // Hide y-axis grid lines
color: function (context) {
return context.tick.value === 0 ? "#334155" : null;
},
drawTicks: false,
},
border: {
color: "#334155",
},
},
},
};
};
/**
* set Time Trend chart data
* @param {array} chartData Time Trend chart conforming data
* @param {number} xMax Time Trend xMax
* @param {number} xMin Time Trend xMin
* @param {number} yMax Time Trend yMax
* @param {number} yMin Time Trend yMin
*/
const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
const max = yMax * 1.1;
const xVal = timeRange(xMin, xMax, 100);
const yVal = yTimeRange(chartData, 100, yMin, yMax);
xVal.map((x, index) => ({ x, y: yVal[index] }));
let formattedXVal = xVal.map((value) => formatTime(value));
formattedXVal = formatMaxTwo(formattedXVal);
const selectTimeMinIndex = getXIndex(xVal, selectDurationTime.value.min);
const selectTimeMaxIndex = getXIndex(xVal, selectDurationTime.value.max);
const start = selectTimeMinIndex;
const end = selectTimeMaxIndex;
const inside = (ctx, value) =>
ctx.p0DataIndex >= start && ctx.p1DataIndex <= end ? value : undefined;
const outside = (ctx, value) =>
ctx.p0DataIndex < start || ctx.p1DataIndex > end ? value : undefined;
timeChartData.value = {
labels: formattedXVal,
datasets: [
{
label: "Conforming",
data: yVal,
fill: true,
showLine: false,
tension: 0.4,
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
pointHitRadius: 0,
spanGaps: true,
segment: {
backgroundColor: (ctx) =>
inside(ctx, "rgb(0,153,255)") || outside(ctx, "rgb(255,170,68)"),
},
x: "x",
y: "y",
},
],
};
timeChartOptions.value = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
legend: false, // Hide legend
tooltip: false,
},
scales: {
x: {
ticks: {
maxRotation: 0, // Do not rotate labels (0~50)
color: "#334155",
display: true,
},
grid: {
display: false, // Hide x-axis grid lines
},
title: {
display: true,
text: "Time",
color: "rgba(100,116,139)",
},
},
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: "rgba(100,116,139)",
drawTicks: false, // Hide extra space on the left
},
border: {
display: false, // Hide the extra border line on the left
},
title: {
display: true,
text: "Occurrences",
color: "rgba(100,116,139)",
},
},
},
};
};
// watch
watch(conformanceTempReportData, (newValue) => {
if (newValue?.rule && newValue.rule.min !== null) {
selectDurationTime.value = {
min: newValue.rule.min,
max: newValue.rule.max,
};
}
data.value = setConformanceTempReportData(newValue);
});
// created - emitter listeners
emitter.on("coverPlate", (boolean) => {
isCoverPlate.value = boolean;
});
// Get selectTimeRange for use by Time Trend
emitter.on(
"timeRangeMaxMin",
(newData) => (selectDurationTime.value = newData),
);
onBeforeUnmount(() => {
emitter.off("coverPlate");
emitter.off("timeRangeMaxMin");
});
</script>
<style scoped>
:deep(.disc) {
font-variation-settings:
"FILL" 1,
"wght" 100,
"GRAD" 0,
"opsz" 20;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
<template>
<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>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ActList
* Checkbox-based activity list for conformance checking input.
*/
import { ref, watch, onBeforeUnmount } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
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.select,
(newValue) => {
actList.value = newValue;
},
);
/** Emits the selected activities list via the event bus. */
function actListData() {
emitter.emit("actListData", actList.value);
}
// created
emitter.on("reset", (data) => {
actList.value = data;
});
onBeforeUnmount(() => {
emitter.off("reset");
});
</script>

View File

@@ -0,0 +1,116 @@
<template>
<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>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ActRadio
* Radio-button activity selector for conformance checking
* start/end activity input.
*/
import { ref, computed, watch, onBeforeUnmount } from "vue";
import { useConformanceInputStore } from "@/stores/conformanceInput";
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 conformanceInputStore = useConformanceInputStore();
const sortData = ref([]);
const localSelect = ref(null);
const selectedRadio = ref(null);
watch(
() => props.data,
(newValue) => {
sortData.value = sortNumEngZhtw(newValue);
},
{ immediate: true },
);
watch(
() => props.task,
(newValue) => {
selectedRadio.value = newValue;
},
);
const inputActivityRadioData = computed(() => ({
category: props.category,
task: selectedRadio.value,
}));
/** 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,
);
}
/** 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,
);
}
// created
sortNumEngZhtw(sortData.value);
localSelect.value = props.isSubmit ? props.select : null;
selectedRadio.value = localSelect.value;
emitter.on("reset", (data) => {
selectedRadio.value = data;
});
setGlobalActivityRadioDataState();
onBeforeUnmount(() => {
emitter.off("reset");
});
</script>

View File

@@ -0,0 +1,217 @@
<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"
>
<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"
>
<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>
</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"
>
<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>
<!-- 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'"
>
<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)"
>
<span>{{ element }}</span>
</div>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop="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>
</div>
</template>
</draggable>
</div>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ActSeqDrag
* Drag-and-drop activity sequence builder for
* conformance rule configuration.
*/
import { ref, computed, onBeforeUnmount } from "vue";
import { sortNumEngZhtw } from "@/module/sortNumEngZhtw.js";
import emitter from "@/utils/emitter";
import { cloneDeep } from "lodash-es";
const props = defineProps(["data", "listSeq", "isSubmit", "category"]);
const listSequence = ref([]);
const lastItemIndex = ref(null);
const isSelect = ref(true);
const datadata = computed(() => {
// Sort the Activity List
let newData;
if (props.data !== null) {
newData = cloneDeep(props.data);
sortNumEngZhtw(newData);
}
return newData;
});
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
function moveActItem(index, element) {
listSequence.value.push(element);
}
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
function moveSeqItem(index, element) {
listSequence.value.splice(index, 1);
}
/**
* get listSequence
*/
function getComponentData() {
emitter.emit("getListSequence", {
category: props.category,
task: listSequence.value,
});
}
/**
* Element dragging started
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
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;
}
/**
* Element dragging ended
*/
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
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 = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
}
// created
const newlist = cloneDeep(props.listSeq);
listSequence.value = props.isSubmit ? newlist : [];
emitter.on("reset", (data) => {
listSequence.value = [];
});
onBeforeUnmount(() => {
emitter.off("reset");
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@apply bg-primary/20;
}
.dragSelected {
@apply !opacity-100;
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<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"
/>
<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"
>
<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"
/>
<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"
>
<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"
/>
<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"
>
<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"
/>
<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"
>
<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"
/>
<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"
>
<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"
/>
<label :for="act.id + act.name" class="ml-2">{{ act.name }}</label>
</div>
</div>
</section>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ConformanceRadioGroup
* Radio button groups for conformance rule type, activity
* sequence, mode, and process scope selection.
*/
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 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" },
];
const activitySequence = [
{ 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)" },
];
const processScope = [
{ 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" },
];
const actSeqFromTo = [
{ 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
}
/** Emits event when the activity sequence radio changes. */
function changeRadioSeq() {
emitter.emit("isRadioSeqChange", true);
}
/** Emits event when the process scope radio changes. */
function changeRadioProcessScope() {
emitter.emit("isRadioProcessScopeChange", true);
}
/** Emits event when the extended activity sequence radio changes. */
function changeRadioActSeqMore() {
emitter.emit("isRadioActSeqMoreChange", true);
}
/** Emits event when the from/to activity sequence radio changes. */
function changeRadioActSeqFromTo() {
emitter.emit("isRadioActSeqFromToChange", true);
}
</script>

View File

@@ -0,0 +1,473 @@
<template>
<div class="px-4 text-sm">
<!-- Have activity -->
<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>
<!-- Activity duration -->
<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>
<!-- 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>
<!-- 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>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ConformanceSelectResult
* Conformance result list with selectable items and
* 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 { cloneDeep } from "lodash-es";
const conformanceStore = useConformanceStore();
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 state = reactive({
containstTasksData: null,
startEndData: null,
selectCfmSeqStart: null,
selectCfmSeqEnd: null,
selectCfmSeqDirectly: [],
selectCfmSeqEventually: [],
durationData: null,
selectCfmPtEteStart: null, // Processing time
selectCfmPtEteEnd: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPStart: null,
selectCfmPtPEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteStart: null, // Waiting time
selectCfmWtEteEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPStart: null,
selectCfmWtPEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteStart: null, // Cycle time
selectCfmCtEteEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
startAndEndIsReset: false,
});
const selectCfmSeqSE = computed(() => {
const data = [];
if (state.selectCfmSeqStart) data.push(state.selectCfmSeqStart);
if (state.selectCfmSeqEnd) data.push(state.selectCfmSeqEnd);
data.sort((a, b) => {
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmPtEteSE = computed(() => {
const data = [];
if (state.selectCfmPtEteSEStart) data.push(state.selectCfmPtEteSEStart);
if (state.selectCfmPtEteSEEnd) data.push(state.selectCfmPtEteSEEnd);
data.sort((a, b) => {
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmPtPSE = computed(() => {
const data = [];
if (state.selectCfmPtPSEStart) data.push(state.selectCfmPtPSEStart);
if (state.selectCfmPtPSEEnd) data.push(state.selectCfmPtPSEEnd);
data.sort((a, b) => {
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmWtEteSE = computed(() => {
const data = [];
if (state.selectCfmWtEteSEStart) data.push(state.selectCfmWtEteSEStart);
if (state.selectCfmWtEteSEEnd) data.push(state.selectCfmWtEteSEEnd);
data.sort((a, b) => {
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmWtPSE = computed(() => {
const data = [];
if (state.selectCfmWtPSEStart) data.push(state.selectCfmWtPSEStart);
if (state.selectCfmWtPSEEnd) data.push(state.selectCfmWtPSEEnd);
data.sort((a, b) => {
const order = { From: 1, To: 2 };
return order[a.category] - order[b.category];
});
return data;
});
const selectCfmCtEteSE = computed(() => {
const data = [];
if (state.selectCfmCtEteSEStart) data.push(state.selectCfmCtEteSEStart);
if (state.selectCfmCtEteSEEnd) data.push(state.selectCfmCtEteSEEnd);
data.sort((a, b) => {
const order = { Start: 1, End: 2 };
return order[a.category] - order[b.category];
});
return data;
});
/**
* All reset
*/
function reset() {
state.containstTasksData = null;
state.startEndData = null;
state.selectCfmSeqStart = null;
state.selectCfmSeqEnd = null;
state.selectCfmSeqDirectly = [];
state.selectCfmSeqEventually = [];
state.durationData = null;
state.selectCfmPtEteStart = null;
state.selectCfmPtEteEnd = null;
state.selectCfmPtEteSEStart = null;
state.selectCfmPtEteSEEnd = null;
state.selectCfmPtPStart = null;
state.selectCfmPtPEnd = null;
state.selectCfmPtPSEStart = null;
state.selectCfmPtPSEEnd = null;
state.selectCfmWtEteStart = null; // Waiting time
state.selectCfmWtEteEnd = null;
state.selectCfmWtEteSEStart = null;
state.selectCfmWtEteSEEnd = null;
state.selectCfmWtPStart = null;
state.selectCfmWtPEnd = null;
state.selectCfmWtPSEStart = null;
state.selectCfmWtPSEEnd = null;
state.selectCfmCtEteStart = null; // Cycle time
state.selectCfmCtEteEnd = null;
state.selectCfmCtEteSEStart = null;
state.selectCfmCtEteSEEnd = null;
state.startAndEndIsReset = true;
}
// created() logic
emitter.on("actListData", (data) => {
state.containstTasksData = data;
});
emitter.on("actRadioData", (newData) => {
const data = cloneDeep(newData);
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"],
};
const updateSelection = (key, mainSelector, secondarySelector) => {
if (state[mainSelector]) {
if (data.task !== state[mainSelector]) state[secondarySelector] = null;
}
data.category = categoryMapping[key][0];
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];
}
});
emitter.on("getListSequence", (data) => {
switch (data.category) {
case "cfmSeqDirectly":
state.selectCfmSeqDirectly = data.task;
break;
case "cfmSeqEventually":
state.selectCfmSeqEventually = data.task;
break;
default:
break;
}
});
emitter.on("reset", (data) => {
reset();
});
// Clear data when switching radio buttons
emitter.on("isRadioChange", (data) => {
if (data) reset();
});
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) reset();
});
emitter.on("isRadioActSeqMoreChange", (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");
});
</script>
<style scoped>
:deep(.disc) {
font-variation-settings:
"FILL" 1,
"wght" 100,
"GRAD" 0,
"opsz" 20;
}
</style>

View File

@@ -0,0 +1,759 @@
<template>
<section class="animate-fadein w-full h-full">
<!-- Have activity -->
<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>
<!-- 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"
/>
<!-- 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"
/>
</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"
/>
</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"
/>
</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"
/>
</div>
</section>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ConformanceShowBar
* Horizontal bar chart component displaying conformance
* check result statistics.
*/
import { ref, computed, watch, onBeforeUnmount } 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,
} = 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 task = ref(null);
const taskStart = ref(null);
const taskEnd = ref(null);
// Activity sequence
const cfmSeqStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
return isEndSelected.value
? setSeqStartAndEndData(cfmSeqEnd.value, "sources", selectedTask)
: cfmSeqStart.value.map((i) => i.label);
});
const cfmSeqEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
return isStartSelected.value
? setSeqStartAndEndData(cfmSeqStart.value, "sinks", selectedTask)
: cfmSeqEnd.value.map((i) => i.label);
});
// Processing time
const cfmPtEteStartData = computed(() => {
return cfmPtEteStart.value.map((i) => i.task);
});
const cfmPtEteEndData = computed(() => {
return cfmPtEteEnd.value.map((i) => i.task);
});
const cfmPtEteSEStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataPtEte?.task ?? null;
return isEndSelected.value
? setStartAndEndData(cfmPtEteSE.value, "end", selectedTask)
: setTaskData(cfmPtEteSE.value, "start");
});
const cfmPtEteSEEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataPtEte?.task ?? null;
return isStartSelected.value
? setStartAndEndData(cfmPtEteSE.value, "start", selectedTask)
: setTaskData(cfmPtEteSE.value, "end");
});
const cfmPtPStartData = computed(() => {
return cfmPtPStart.value.map((i) => i.task);
});
const cfmPtPEndData = computed(() => {
return cfmPtPEnd.value.map((i) => i.task);
});
const cfmPtPSEStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataPtP?.task ?? null;
return isEndSelected.value
? setStartAndEndData(cfmPtPSE.value, "end", selectedTask)
: setTaskData(cfmPtPSE.value, "start");
});
const cfmPtPSEEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataPtP?.task ?? null;
return isStartSelected.value
? setStartAndEndData(cfmPtPSE.value, "start", selectedTask)
: setTaskData(cfmPtPSE.value, "end");
});
// Waiting time
const cfmWtEteStartData = computed(() => {
return cfmWtEteStart.value.map((i) => i.task);
});
const cfmWtEteEndData = computed(() => {
return cfmWtEteEnd.value.map((i) => i.task);
});
const cfmWtEteSEStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataWtEte?.task ?? null;
return isEndSelected.value
? setStartAndEndData(cfmWtEteSE.value, "end", selectedTask)
: setTaskData(cfmWtEteSE.value, "start");
});
const cfmWtEteSEEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataWtEte?.task ?? null;
return isStartSelected.value
? setStartAndEndData(cfmWtEteSE.value, "start", selectedTask)
: setTaskData(cfmWtEteSE.value, "end");
});
const cfmWtPStartData = computed(() => {
return cfmWtPStart.value.map((i) => i.task);
});
const cfmWtPEndData = computed(() => {
return cfmWtPEnd.value.map((i) => i.task);
});
const cfmWtPSEStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataWtP?.task ?? null;
return isEndSelected.value
? setStartAndEndData(cfmWtPSE.value, "end", selectedTask)
: setTaskData(cfmWtPSE.value, "start");
});
const cfmWtPSEEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataWtP?.task ?? null;
return isStartSelected.value
? setStartAndEndData(cfmWtPSE.value, "start", selectedTask)
: setTaskData(cfmWtPSE.value, "end");
});
// Cycle time
const cfmCtEteStartData = computed(() => {
return cfmCtEteStart.value.map((i) => i.task);
});
const cfmCtEteEndData = computed(() => {
return cfmCtEteEnd.value.map((i) => i.task);
});
const cfmCtEteSEStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataCt?.task ?? null;
return isEndSelected.value
? setStartAndEndData(cfmCtEteSE.value, "end", selectedTask)
: setTaskData(cfmCtEteSE.value, "start");
});
const cfmCtEteSEEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataCt?.task ?? null;
return isStartSelected.value
? setStartAndEndData(cfmCtEteSE.value, "start", selectedTask)
: 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;
},
);
/**
* Sets the start and end radio data.
* @param {object} data - Activities list data from the backend (cfmSeqStart, cfmSeqEnd, cfmPtEteSE, etc.).
* @param {string} category - 'start' or 'end'.
* @returns {array}
*/
function setTaskData(data, category) {
let newData = data.map((i) => i[category]);
newData = [...new Set(newData)]; // Set is a collection type that only stores unique values.
return newData;
}
/**
* Resets the linked start and end radio data.
* @param {object} data - Activities list data from the backend (cfmPtEteSE, cfmPtPSE, etc.).
* @param {string} category - 'start' or 'end'.
* @param {string} task - The selected activity task.
* @returns {array}
*/
function setStartAndEndData(data, category, taskVal) {
let oppositeCategory = "";
if (category === "start") {
oppositeCategory = "end";
} else {
oppositeCategory = "start";
}
let newData = data
.filter((i) => i[category] === taskVal)
.map((i) => i[oppositeCategory]);
newData = [...new Set(newData)];
return newData;
}
/**
* Resets the activity sequence linked start and end radio data.
* @param {object} data - Activities list data from the backend (cfmSeqStart or cfmSeqEnd).
* @param {string} category - 'sources' or 'sinks'.
* @param {string} task - The selected activity task.
* @returns {array}
*/
function setSeqStartAndEndData(data, category, taskVal) {
let newData = data.filter((i) => i.label === taskVal).map((i) => i[category]);
newData = [...new Set(...newData)];
return newData;
}
/**
* select start list's task
* @param {Event} e - The input event.
*/
function selectStart(e) {
taskStart.value = e;
if (isStartSelected.value === null || isStartSelected.value === true) {
isStartSelected.value = true;
isEndSelected.value = false;
task.value = e;
taskEnd.value = null;
}
}
/**
* select End list's task
* @param {Event} e - The input event.
*/
function selectEnd(e) {
taskEnd.value = e;
if (isEndSelected.value === null || isEndSelected.value === true) {
isEndSelected.value = true;
isStartSelected.value = false;
task.value = e;
taskStart.value = null;
}
}
/**
* reset all data.
*/
function reset() {
task.value = null;
isStartSelected.value = null;
isEndSelected.value = null;
taskStart.value = null;
taskEnd.value = null;
}
/**
* Updates linked Start & End data when radio selection changes.
* @param {boolean} data - Whether data should be restored from submission state.
*/
function setResetData(data) {
if (data) {
if (props.isSubmit) {
switch (selectedRuleType.value) {
case "Activity sequence":
task.value = props.isSubmitShowDataSeq.task;
isStartSelected.value = props.isSubmitShowDataSeq.isStartSelected;
isEndSelected.value = props.isSubmitShowDataSeq.isEndSelected;
break;
case "Processing time":
switch (selectedProcessScope.value) {
case "End to end":
task.value = props.isSubmitShowDataPtEte.task;
isStartSelected.value =
props.isSubmitShowDataPtEte.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtEte.isEndSelected;
break;
case "Partial":
task.value = props.isSubmitShowDataPtP.task;
isStartSelected.value = props.isSubmitShowDataPtP.isStartSelected;
isEndSelected.value = props.isSubmitShowDataPtP.isEndSelected;
break;
default:
break;
}
break;
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":
task.value = props.isSubmitShowDataCt.task;
isStartSelected.value = props.isSubmitShowDataCt.isStartSelected;
isEndSelected.value = props.isSubmitShowDataCt.isEndSelected;
break;
default:
break;
}
} else {
reset();
}
}
}
// created() logic
emitter.on("isRadioChange", (data) => {
setResetData(data);
});
emitter.on("isRadioSeqChange", (data) => {
setResetData(data);
});
emitter.on("isRadioProcessScopeChange", (data) => {
if (data) {
setResetData(data);
}
});
emitter.on("isRadioActSeqMoreChange", (data) => {
if (data) {
setResetData(data);
}
});
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
setResetData(data);
}
});
emitter.on("reset", (data) => {
reset();
});
onBeforeUnmount(() => {
emitter.off("isRadioChange");
emitter.off("isRadioSeqChange");
emitter.off("isRadioProcessScopeChange");
emitter.off("isRadioActSeqMoreChange");
emitter.off("isRadioActSeqFromToChange");
emitter.off("reset");
});
</script>

View File

@@ -0,0 +1,673 @@
<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">
<!-- 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"
/>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ConformanceTimeRange
* Time range picker for conformance time-based rule
* configuration with calendar inputs.
*/
import { reactive, onBeforeUnmount } 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 { cloneDeep } from "lodash-es";
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,
} = 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 emit = defineEmits(["min-total-seconds", "max-total-seconds"]);
const state = reactive({
timeDuration: null, // Activity duration
timeCfmPtEteAll: null, // Processing time
timeCfmPtEteAllDefault: null,
timeCfmPtEteStart: null,
timeCfmPtEteEnd: null,
timeCfmPtEteSE: null,
timeCfmPtPStart: null,
timeCfmPtPEnd: null,
timeCfmPtPSE: null,
timeCfmWtEteAll: null, // Waiting time
timeCfmWtEteAllDefault: null,
timeCfmWtEteStart: null,
timeCfmWtEteEnd: null,
timeCfmWtEteSE: null,
timeCfmWtPStart: null,
timeCfmWtPEnd: null,
timeCfmWtPSE: null,
timeCfmCtEteAll: null, // Cycle time
timeCfmCtEteAllDefault: null,
timeCfmCtEteStart: null,
timeCfmCtEteEnd: null,
timeCfmCtEteSE: null,
selectCfmPtEteSEStart: null,
selectCfmPtEteSEEnd: null,
selectCfmPtPSEStart: null,
selectCfmPtPSEEnd: null,
selectCfmWtEteSEStart: null,
selectCfmWtEteSEEnd: null,
selectCfmWtPSEStart: null,
selectCfmWtPSEEnd: null,
selectCfmCtEteSEStart: null,
selectCfmCtEteSEEnd: null,
});
// Store refs lookup for dynamic access in handleSingleSelection/handleDoubleSelection
const storeRefs = {
cfmPtEteStart,
cfmPtEteEnd,
cfmPtEteSE,
cfmPtPStart,
cfmPtPEnd,
cfmPtPSE,
cfmWtEteStart,
cfmWtEteEnd,
cfmWtEteSE,
cfmWtPStart,
cfmWtPEnd,
cfmWtPSE,
cfmCtEteStart,
cfmCtEteEnd,
cfmCtEteSE,
};
/**
* get min total seconds
* @param {number} e - The minimum total seconds.
*/
function minTotalSeconds(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);
}
/**
* get Time Range(duration)
* @param {Array} data - Activity list data from the API.
* @param {string} category - 'act', 'single', or 'double'.
* @param {string} task select Radio task or start
* @param {string} taskTwo end
* @returns {object} {min:12, max:345}
*/
function getDurationTime(data, category, task, taskTwo) {
let result = { min: 0, max: 0 };
switch (category) {
case "act":
data.forEach((i) => {
if (i.label === task) {
result = i.duration;
}
});
break;
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) {
result = i.time;
}
});
break;
case "all":
result = data;
break;
default:
break;
}
return result;
}
/**
* All reset
*/
function reset() {
state.timeDuration = null; // Activity duration
state.timeCfmPtEteAll = state.timeCfmPtEteAllDefault; // Processing time
state.timeCfmPtEteStart = null;
state.timeCfmPtEteEnd = null;
state.timeCfmPtEteSE = null;
state.timeCfmPtPStart = null;
state.timeCfmPtPEnd = null;
state.timeCfmPtPSE = null;
state.timeCfmWtEteAll = state.timeCfmWtEteAllDefault; // Waiting time
state.timeCfmWtEteStart = null;
state.timeCfmWtEteEnd = null;
state.timeCfmWtEteSE = null;
state.timeCfmWtPStart = null;
state.timeCfmWtPEnd = null;
state.timeCfmWtPSE = null;
state.timeCfmCtEteAll = state.timeCfmCtEteAllDefault; // Cycle time
state.timeCfmCtEteStart = null;
state.timeCfmCtEteEnd = null;
state.timeCfmCtEteSE = null;
state.selectCfmPtEteSEStart = null;
state.selectCfmPtEteSEEnd = null;
state.selectCfmPtPSEStart = null;
state.selectCfmPtPSEEnd = null;
state.selectCfmWtEteSEStart = null;
state.selectCfmWtEteSEEnd = null;
state.selectCfmWtPSEStart = null;
state.selectCfmWtPSEEnd = null;
state.selectCfmCtEteSEStart = null;
state.selectCfmCtEteSEEnd = null;
}
// created() logic
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],
);
}
};
const handleSingleSelection = (key, timeKey, durationType) => {
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 = cloneDeep(state.timeCfmPtEteAll);
break;
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = cloneDeep(state.timeCfmWtEteAll);
break;
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = cloneDeep(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 = cloneDeep(state.timeCfmPtEteAll);
break;
case "Waiting time":
state.timeCfmWtEteAll = getDurationTime(cfmWtEteWhole.value, "all");
state.timeCfmWtEteAllDefault = cloneDeep(state.timeCfmWtEteAll);
break;
case "Cycle time":
state.timeCfmCtEteAll = getDurationTime(cfmCtEteWhole.value, "all");
state.timeCfmCtEteAllDefault = cloneDeep(state.timeCfmCtEteAll);
break;
default:
break;
}
} else reset();
}
});
emitter.on("isRadioActSeqFromToChange", (data) => {
if (data) {
reset();
}
});
onBeforeUnmount(() => {
emitter.off("actRadioData");
emitter.off("reset");
emitter.off("isRadioChange");
emitter.off("isRadioProcessScopeChange");
emitter.off("isRadioActSeqMoreChange");
emitter.off("isRadioActSeqFromToChange");
});
</script>

View File

@@ -0,0 +1,33 @@
<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"
>
<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>
</li>
</ul>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ResultArrow
* Conformance result display with arrow icons showing activity
* sequences.
*/
defineProps(["data", "select"]);
</script>

View File

@@ -0,0 +1,58 @@
<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 displayData"
: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>
</li>
</ul>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ResultCheck
* Conformance result display with check-circle icons showing
* matched activities.
*/
import { ref, watch, onBeforeUnmount } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(["data", "select"]);
const displayData = ref(props.select);
watch(
() => props.data,
(newValue) => {
displayData.value = newValue;
},
);
watch(
() => props.select,
(newValue) => {
displayData.value = newValue;
},
);
emitter.on("reset", (val) => (displayData.value = val));
onBeforeUnmount(() => {
emitter.off("reset");
});
</script>

View File

@@ -0,0 +1,52 @@
<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
>
<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
>
</li>
</ul>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/ResultDot
* Conformance result display with dot icons showing category
* and task pairs.
*/
import { ref, watch, onBeforeUnmount } from "vue";
import emitter from "@/utils/emitter";
const props = defineProps(["timeResultData", "select"]);
const data = ref(props.select);
watch(
() => props.timeResultData,
(newValue) => {
data.value = newValue;
},
{ deep: true },
);
emitter.on("reset", (val) => (data.value = val));
onBeforeUnmount(() => {
emitter.off("reset");
});
</script>

View File

@@ -0,0 +1,113 @@
<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>
<span>~</span>
<Durationjs
:max="maxVuemax"
:min="maxVuemin"
:size="'max'"
:updateMin="updateMin"
@total-seconds="maxTotalSeconds"
:value="durationMax"
>
</Durationjs>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/ConformanceSidebar/TimeRangeDuration
* Time range duration picker with min/max duration inputs
* for conformance time-based rules.
*/
import { ref, watch } from "vue";
import Durationjs from "@/components/DurationInput.vue";
import { cloneDeep } from "lodash-es";
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);
const timeRangeMax = ref(0);
const minVuemin = ref(0);
const minVuemax = ref(0);
const maxVuemin = ref(0);
const maxVuemax = ref(0);
const updateMax = ref(null);
const updateMin = ref(null);
const durationMin = ref(null);
const durationMax = ref(null);
/** Deep-copies timeData min/max values to the Vue component boundaries. */
function setTimeValue() {
// Deep copy the original timeData values
minVuemin.value = cloneDeep(timeData.value.min);
minVuemax.value = cloneDeep(timeData.value.max);
maxVuemin.value = cloneDeep(timeData.value.min);
maxVuemax.value = cloneDeep(timeData.value.max);
}
/**
* Handles the minimum duration total seconds update.
* @param {number} e - The total seconds from the min duration component.
*/
function minTotalSeconds(e) {
timeRangeMin.value = e;
updateMin.value = e;
emit("min-total-seconds", e);
}
/**
* Handles the maximum duration total seconds update.
* @param {number} e - The total seconds from the max duration component.
*/
function maxTotalSeconds(e) {
timeRangeMax.value = e;
updateMax.value = 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 },
);
// created
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) {
durationMin.value = props.select.rule.min;
durationMax.value = props.select.rule.max;
}
}
</script>

View File

@@ -0,0 +1,447 @@
<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>
</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>
</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 in columnData" :key="col.field">
<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>
</template>
</Column>
</div>
</DataTable>
</div>
</section>
</div>
</Dialog>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Conformance/MoreModal
* Modal dialog showing detailed conformance check
* results with expandable activity sequences.
*/
import { ref, computed, watch, nextTick, useTemplateRef, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useConformanceStore } from "@/stores/conformance";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
import { cloneDeep } from "lodash-es";
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");
// data
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 cyTraceInstance = ref(null);
const processMap = ref({
nodes: [],
edges: [],
});
// computed
const traceTotal = computed(() => {
return traceList.value.length;
});
const traceList = computed(() => {
const sum = props.listTraces
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
if (sum === 0) return [];
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) {
const data = cloneDeep(infiniteData.value);
data.forEach((item) => {
item.facets.forEach((facet, index) => {
item[`fac_${index}`] = facet.value; // Create a new key-value pair
});
delete item.facets; // Remove the original facets property
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;
}
return [];
});
const columnData = computed(() => {
const data = cloneDeep(props.cases);
if (!data || data.length === 0) return [];
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,
})),
];
return result;
});
// watch
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.traceId,
(newValue) => {
// Update showTraceId when the traceId prop changes
showTraceId.value = newValue;
},
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && isScrollTop.scrollTop !== undefined)
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(
() => props.firstCases,
(newValue) => {
infiniteData.value = newValue;
},
);
watch(infinite404, (newValue) => {
if (newValue === 404) maxItems.value = true;
});
// methods
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val) {
if (Number((val * 100).toFixed(1)) >= 100) return 100;
else return Number.parseFloat((val * 100).toFixed(1));
}
/**
* set progress bar width
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* switch case data
* @param {number} id case id
*/
async function switchCaseData(id) {
if (id === showTraceId.value) return;
infinite404.value = null;
maxItems.value = false;
startNum.value = 0;
let result = null;
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 = 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() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
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",
height: 80,
width: 100,
},
});
});
}
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData() {
processMap.value.edges = [];
if (props.taskSeq !== null) {
props.taskSeq.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
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() {
nextTick(() => {
const graphId = cfmTrace.value;
setNodesData();
setEdgesData();
cyTraceInstance.value?.destroy();
if (graphId !== null)
cyTraceInstance.value = cytoscapeMapTrace(
processMap.value.nodes,
processMap.value.edges,
graphId,
);
});
}
/**
* Infinite scroll: loads more data.
*/
async function fetchData() {
try {
infiniteFinish.value = false;
startNum.value += 20;
const result = await conformanceStore.getConformanceTraceDetail(
props.listNo,
showTraceId.value,
startNum.value,
);
if (result) infiniteData.value = [...infiniteData.value, ...result];
infiniteFinish.value = true;
} catch (error) {
console.error("Failed to load data:", error);
infiniteFinish.value = true;
}
}
/**
* Infinite scroll: listens for scroll reaching the bottom.
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if (
maxItems.value ||
(infiniteData.value?.length ?? 0) < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight =
container.scrollTop + container.clientHeight + 20 >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
onBeforeUnmount(() => {
cyTraceInstance.value?.destroy();
});
</script>
<style scoped>
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@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;
}
/* Center header title */
:deep(.p-column-header-content) {
@apply justify-center;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
max-width: 184px;
overflow-wrap: break-word;
word-wrap: break-word;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<!-- Activity List -->
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Activity List&nbsp;({{ data.length }})</p>
</div>
<!-- Table -->
<div
class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]"
>
<table
class="border-separate border-spacing-x-2 table-auto min-w-full 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>
</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"
>
<template #item="{ element, index }">
<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>
</td>
<td class="px-4 py-2 text-right">{{ element.occurrences }}</td>
<td class="px-4 py-2 text-right">
{{ element.occurrence_ratio }}
</td>
</tr>
</template>
</Draggable>
</table>
</div>
</div>
<!-- Sequence -->
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative 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>
<!-- 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'"
>
<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>
<span
class="material-symbols-outlined pl-1 cursor-pointer duration-300 hover:text-danger"
@click.stop="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>
</div>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/ActAndSeq Activity list
* with drag-and-drop sequence builder for creating filter
* rules.
*/
import { ref, computed, watch } from "vue";
import { sortNumEngZhtwForFilter } from "@/module/sortNumEngZhtw.js";
const props = defineProps({
filterTaskData: {
type: Array,
required: true,
},
progressWidth: {
type: Function,
required: false,
},
listSeq: {
type: Array,
required: true,
},
});
const emit = defineEmits(["update:listSeq"]);
const listSequence = ref([]);
const filteredData = ref(props.filterTaskData);
const lastItemIndex = ref(null);
const data = computed(() => {
// Sort the Activity List
return [...filteredData.value].sort((x, y) => {
const diff = y.occurrences - x.occurrences;
return diff === 0 ? sortNumEngZhtwForFilter(x.label, y.label) : diff;
});
});
watch(
() => props.listSeq,
(newval) => {
listSequence.value = newval;
},
);
watch(
() => props.filterTaskData,
(newval) => {
filteredData.value = newval;
},
);
/**
* Moves an activity from the list to the sequence on double-click.
* @param {number} index - The item index in the activity list.
* @param {Object} element - The activity data object.
*/
function moveActItem(index, element) {
listSequence.value.push(element);
}
/**
* Removes an activity from the sequence on double-click.
* @param {number} index - The item index in the sequence.
* @param {Object} element - The activity data object.
*/
function moveSeqItem(index, element) {
listSequence.value.splice(index, 1);
}
/** Emits the current sequence list to the parent component. */
function getComponentData() {
emit("update:listSeq", listSequence.value);
}
/**
* Handles drag start: hides original element and last arrow.
* @param {Event} evt - The drag start event.
*/
function onStart(evt) {
const lastChild = evt.to.lastChild.lastChild;
lastChild.style.display = "none";
// Hide the dragged element at its original position
const originalElement = evt.item;
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;
}
/**
* Handles drag end: restores element visibility.
* @param {Event} evt - The drag end event.
*/
function onEnd(evt) {
// Show the dragged element
const originalElement = evt.item;
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 = "";
}
// Reset: hide the second-to-last element's arrow when dragging the last element
lastItemIndex.value = null;
}
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
.ghostSelected {
@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;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div
class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full"
>
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">{{ tableTitle }}&nbsp;({{ tableData.length }})</p>
</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"
>
<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"
/>
</Row>
</ColumnGroup>
<Column selectionMode="single" bodyClass="!p-2 !border-0"></Column>
<Column
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="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>
</template>
</Column>
<Column
field="occurrences"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occurrence_ratio"
header="Occurrence Ratio"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/ActOcc Activity
* occurrences filter table with single-row radio selection.
*/
import { ref, watch } from "vue";
const props = defineProps({
tableTitle: {
type: String,
required: true,
},
tableData: {
type: Array,
required: true,
},
tableSelect: {
type: [Object, Array],
default: null,
},
progressWidth: {
type: Function,
required: false,
},
});
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const metaKey = ref(true);
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);
}
</script>

View File

@@ -0,0 +1,158 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 h-full">
<div class="flex justify-between items-center my-2">
<p class="h2">{{ tableTitle }}&nbsp;({{ data.length }})</p>
</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"
>
<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"
/>
</Row>
</ColumnGroup>
<Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column>
<Column
field="label"
header="Activity"
bodyClass="break-words !py-2 !border-0"
></Column>
<Column header="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>
</template>
</Column>
<Column
field="occurrences"
header="Occurrences"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="occurrence_ratio"
header="O2"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column header="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>
</template>
</Column>
<Column
field="cases"
header="Cases with Activity"
bodyClass="!text-right !py-2 !border-0"
></Column>
<Column
field="case_ratio"
header="C2"
bodyClass="!text-right !py-2 !border-0"
></Column>
</DataTable>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/ActOccCase Activity
* occurrences and cases filter table with multi-row checkbox
* selection.
*/
import { ref, watch } from "vue";
const props = defineProps([
"tableTitle",
"tableData",
"tableSelect",
"progressWidth",
]);
const emit = defineEmits(["on-row-select"]);
const select = ref(null);
const data = ref(props.tableData);
watch(
() => props.tableSelect,
(newval) => {
select.value = newval;
},
);
/** Emits the current selection when a row is selected. */
function onRowSelect() {
emit("on-row-select", select.value);
}
/** Emits the current selection when a row is unselected. */
function onRowUnselect() {
emit("on-row-select", select.value);
}
/**
* Handles select-all rows action.
* @param {Event} e - The select-all event with data property.
*/
function onRowSelectAll(e) {
select.value = e.data;
emit("on-row-select", select.value);
}
/**
* Handles unselect-all rows action.
* @param {Event} e - The unselect-all event.
*/
function onRowUnelectAll(e) {
select.value = null;
emit("on-row-select", select.value);
}
</script>

View File

@@ -0,0 +1,964 @@
<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"
>
{{ 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="onRowSelectionChange"
>
<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="onRowSelectionChange"
@row-unselect="onRowSelectionChange"
@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>
<!-- type: value -->
<div
v-else-if="valueTypes.has(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>
<!-- 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>
<DatePicker
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>
<DatePicker
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>
</section>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
// imacat.yang@dsp.im (imacat), 2023/9/23
/**
* @module components/Discover/Map/Filter/Attributes
* Case attribute filter with dynamic form fields
* 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";
const emit = defineEmits(["select-attribute"]);
const allMapDataStore = useAllMapDataStore();
const { filterAttrs } = storeToRefs(allMapDataStore);
const selectedAttName = ref({});
const selectedAttRange = ref(null);
const valueTypes = new Set(["int", "float", "date"]);
const classTypes = new Set(["boolean", "string"]);
const chartData = ref({});
const chartOptions = ref({});
const chartComplete = ref(null); // Rendered chart.js instance data
const selectArea = ref(null);
const selectRange = ref(1000); // Number of divisions for the selection range
const startTime = ref(null); // PrimeVue Calendar v-model
const endTime = ref(null); // PrimeVue Calendar v-model
const startMinDate = ref(null);
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 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 panelProps = {
onClick: (event) => {
event.stopPropagation();
},
};
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",
},
};
const attTotal = computed(() => {
return filterAttrs.value.length;
});
const attRangeTotal = computed(() => {
const type = selectedAttName.value.type;
let result = null; // Initialize the result variable with null
if (classTypes.has(type) && attRangeData.value) {
result = `(${attRangeData.value.length})`; // Assign the length of attRangeData if it exists
}
return result;
});
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);
data = selectedAttName.value.options.map((item, index) => {
const ratio = item.freq / sum;
const result = {
id: index + 1,
key: selectedAttName.value.key,
type: type,
value: item.value,
occ_progress_bar: ratio * 100,
occ_value: item.freq.toLocaleString("en-US"),
occ_ratio: getPercentLabel(ratio),
freq: item.freq,
};
result.label = null;
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.has(selectedAttName.value.type)) {
const data = filterAttrs.value.find(
(item) =>
item.type === selectedAttName.value.type &&
item.key === selectedAttName.value.key,
);
return data ?? null;
}
return null;
});
// Compute slider data; time format: millisecond timestamps
const sliderDataComputed = computed(() => {
if (!valueData.value) return [];
let xAxisMin;
let xAxisMax;
const min = valueData.value.min;
const max = valueData.value.max;
const type = valueData.value.type;
switch (type) {
case "dummy":
case "date":
xAxisMin = new Date(min).getTime();
xAxisMax = new Date(max).getTime();
break;
default:
xAxisMin = min;
xAxisMax = max;
break;
}
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
let data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + step * i);
}
switch (type) {
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) => {
let result = new Decimal(value.toFixed(2)).toNumber();
result = result === -0 ? 0 : result;
return result;
});
break;
default:
break;
}
return data;
});
// user select value type start and end
const attValueTypeStartEnd = computed(() => {
let start;
let end;
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");
break;
default:
start = valueStart.value;
end = valueEnd.value;
break;
}
const data = {
// Data to send to the backend
type: type,
data: {
key: selectedAttName.value.key,
min: start,
max: end,
},
};
emit("select-attribute", data);
return [start, end];
});
const labelsData = computed(() => {
if (!valueData.value) return [];
const min = new Date(valueData.value.min).getTime();
const max = new Date(valueData.value.max).getTime();
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
return data;
});
/**
* Emits the current attribute selection when a row is selected or deselected.
*/
function onRowSelectionChange() {
const type = selectedAttName.value.type;
const data = {
type: type,
data: selectedAttRange.value,
};
emit("select-attribute", data);
}
/**
* Selects all options in the categorical table.
* @param {Event} e - The input event.
*/
function onRowSelectAll(e) {
selectedAttRange.value = e.data;
const type = selectedAttName.value.type;
const data = {
type: type,
data: selectedAttRange.value,
};
emit("select-attribute", data);
}
/**
* Deselects all options in the categorical table.
*/
function onRowUnelectAll() {
selectedAttRange.value = null;
const type = selectedAttName.value.type;
const data = {
type: type,
data: selectedAttRange.value,
};
emit("select-attribute", data);
}
/**
* Switches the attribute name radio selection.
* @param {Event} e - The input event.
*/
function switchAttNameRadio(e) {
selectedAttRange.value = null;
startTime.value = null;
endTime.value = null;
valueStart.value = null;
valueEnd.value = null;
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":
// Clear two-way bindings except for date
valueStart.value = null;
valueEnd.value = null;
// Initialize Calendar
startMinDate.value = new Date(min);
startMaxDate.value = new Date(max);
endMinDate.value = new Date(min);
endMaxDate.value = new Date(max);
// Initialize: set the calendar range to match the timeline range
startTime.value = new Date(min);
endTime.value = new Date(max);
break;
default:
// Clear date two-way bindings
startTime.value = null;
endTime.value = null;
// Initialize InputNumber
valueStartMin.value = min;
valueStartMax.value = max;
valueEndMin.value = min;
valueEndMax.value = max;
// Initialize: set the InputNumber range to match the timeline range
valueStart.value = min;
valueEnd.value = max;
break;
}
// Send to backend
// attValueTypeStartEnd.value; should this function be called? sonar-qube
// Create chart
createChart();
}
}
/**
* set progress bar width
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val) {
if (Number((val * 100).toFixed(1)) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
/**
* Adjusts the overlay mask size.
* @param {object} chart - The Chart.js instance data.
*/
function resizeMask(chart) {
if (selectRange.value === 0) return;
const from = (selectArea.value[0] * 0.01) / (selectRange.value * 0.01);
const to = (selectArea.value[1] * 0.01) / (selectRange.value * 0.01);
resizeLeftMask(chart, from);
resizeRightMask(chart, to);
}
/**
* Adjusts the left overlay mask size.
* @param {object} chart - The Chart.js instance data.
*/
function resizeLeftMask(chart, from) {
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-left");
if (!canvas || !mask) return;
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
}
/**
* Adjusts the right overlay mask size.
* @param {object} chart - The Chart.js instance data.
*/
function resizeRightMask(chart, to) {
const canvas = document.querySelector("#chartCanvasId canvas");
const mask = document.getElementById("chart-mask-right");
if (!canvas || !mask) return;
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
}
/**
* Creates and renders the Chart.js line chart for attribute data.
*/
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 minX = vData.chart.x_axis.min;
const maxX = vData.chart.x_axis.max;
let setChartData = {};
let setChartOptions = {};
let setLabels = [];
switch (vData.type) {
case "int":
setLabels = data.map((item) => Math.round(item.x));
break;
case "float":
setLabels = data.map((item, index) => {
let x;
if (index === 0) {
x = Math.floor(item.x * 100) / 100;
} else if (index === data.length - 1) {
item.x = Math.ceil(item.x * 100) / 100;
x = item.x;
} else {
x = Math.round(item.x * 100) / 100;
}
return x;
});
break;
case "date":
setLabels = labelsData.value;
break;
default:
break;
}
setChartData = {
datasets: [
{
label: "Attribute Value",
data: data,
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
},
],
labels: setLabels,
};
setChartOptions = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false,
},
title: false,
},
animation: {
onComplete: (e) => {
chartComplete.value = e.chart;
resizeMask(e.chart);
},
},
interaction: {
intersect: true,
},
scales: {
y: {
beginAtZero: true, // Scale includes 0
max: max,
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
},
},
},
};
if (isDateType) {
setChartOptions.scales.x = {
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",
display: true,
source: "labels", // Flexibly display label count proportionally
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
minUnit: "day", // Minimum display unit
// displayFormats: {
// minute: 'HH:mm MMM d',
// hour: 'HH:mm MMM d',
// }
},
};
} else {
setChartOptions.scales.x = {
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) => {
let x;
switch (vData.type) {
case "int":
return Math.round(value);
case "float":
switch (index) {
case 0:
x = Math.floor(value * 100) / 100;
break;
case values.length - 1:
x = Math.ceil(value * 100) / 100;
break;
default:
x = Math.round(value * 100) / 100;
}
// 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;
}
},
},
grid: {
display: false, // Hide x-axis grid lines
},
};
}
chartData.value = setChartData;
chartOptions.value = setChartOptions;
}
/**
* Handles slider value changes.
* @param {array} e [1, 100]
*/
function changeSelectArea(e) {
// When the calendar changes, the slider follows
const sliderData = sliderDataComputed.value;
const start = sliderData[e[0].toFixed()];
const end = sliderData[e[1].toFixed()]; // Get the index, which must be an integer.
switch (selectedAttName.value.type) {
case "dummy":
case "date":
startTime.value = new Date(start);
endTime.value = new Date(end);
// Reset the start/end calendar selection range
endMinDate.value = new Date(start);
startMaxDate.value = new Date(end);
break;
default:
valueStart.value = start;
valueEnd.value = end;
// Reset the start/end selection range
valueEndMin.value = start;
valueStartMax.value = end;
break;
}
// Recalculate the chart mask
resizeMask(chartComplete.value);
// Execute timeFrameStartEnd to update the data
// attValueTypeStartEnd.value; should this function be called? sonar-qube
}
/**
* Updates the slider and chart when a start or end time is selected.
* @param {object} e - The date object or blur event.
* @param {string} direction start or end
*/
function sliderValueRange(e, direction) {
// Find the closest index; time format: millisecond timestamps
const sliderData = sliderDataComputed.value;
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]];
const closestIndexes = targetTime.map((target) => {
let closestIndex = 0;
closestIndex =
((target - sliderData[0]) /
(sliderData[sliderData.length - 1] - sliderData[0])) *
sliderData.length;
let result = Math.round(Math.abs(closestIndex));
result = Math.min(result, selectRange.value);
return result;
});
// Update the slider
selectArea.value = closestIndexes;
// Reset the start/end calendar selection range
if (!isDateType) inputValue = Number(e.value.replaceAll(",", ""));
if (direction === "start") {
if (isDateType) {
endMinDate.value = e;
} else {
valueEndMin.value = inputValue;
}
} else if (direction === "end") {
if (isDateType) {
startMaxDate.value = e;
} else {
valueStartMax.value = inputValue;
}
}
// Recalculate the chart mask
if (!Number.isNaN(closestIndexes[0]) && !Number.isNaN(closestIndexes[1]))
resizeMask(chartComplete.value);
else return;
}
// created() equivalent
emitter.on("map-filter-reset", (value) => {
if (value) {
selectedAttRange.value = null;
if (valueData.value && valueTypes.has(selectedAttName.value.type)) {
const min = valueData.value.min;
const max = valueData.value.max;
startTime.value = new Date(min);
endTime.value = new Date(max);
valueStart.value = min;
valueEnd.value = max;
selectArea.value = [0, selectRange.value];
resizeMask(chartComplete.value);
}
}
});
onMounted(() => {
// Slider
selectArea.value = [0, selectRange.value]; // Initialize the slider
});
onBeforeUnmount(() => {
selectedAttName.value = {};
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;
}
</style>

View File

@@ -0,0 +1,197 @@
<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">
<ToggleSwitch
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>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/Funnel Filter funnel
* panel showing applied filter rules with toggle, delete, and
* 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";
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);
/**
* 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) {
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] = rule;
else temporaryData.value[index] = 0;
}
/**
* Deletes a single filter rule or all rules.
* @param {number|string} index - The rule index, or 'all' to delete all.
*/
async function deleteRule(index) {
if (index === "all") {
temporaryData.value = [];
isRuleData.value = [];
ruleData.value = [];
if (tempFilterId.value) {
isLoading.value = true;
tempFilterId.value = null;
await allMapDataStore.getAllMapData();
await allMapDataStore.getAllTrace(); // SidebarTrace needs to update in sync
emit("submit-all");
isLoading.value = false;
}
$toast.success("Filter(s) deleted.");
} else {
$toast.success(`Filter deleted.`);
temporaryData.value.splice(index, 1);
isRuleData.value.splice(index, 1);
ruleData.value.splice(index, 1);
}
}
/** 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");
await allMapDataStore.checkHasResult(); // Quick backend check for results
if (hasResultRule.value === null) {
return;
} 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) {
allMapDataStore.traceId = allMapDataStore.traces[0]?.id;
}
emit("submit-all");
isLoading.value = false;
$toast.success("Filter(s) applied.");
return;
}
// sonar-qube "This statement will not be executed conditionally"
isLoading.value = true;
await delaySecond(1);
isLoading.value = false;
$toast.warning("No result.");
}
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
/* TimeLine */
:deep(.p-timeline) {
@apply leading-none my-4;
}
:deep(.p-timeline-event-opposite) {
@apply hidden;
}
:deep(.p-timeline-event-separator) {
@apply mx-4;
}
:deep(.p-timeline-event-marker) {
@apply !bg-primary !border-primary !w-2 !h-2;
}
:deep(.p-timeline-event-connector) {
@apply !bg-primary my-2 !w-[1px];
}
:deep(.p-timeline-event-content) {
@apply !px-0;
}
</style>

View File

@@ -0,0 +1,419 @@
<template>
<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">
<span class="material-symbols-outlined mr-2 !text-base">info</span>
<p>Select or fill in a time range.</p>
</div>
<div class="chartContainer h-3/5 relative">
<canvas id="chartCanvasId"></canvas>
<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 class="px-2 py-3">
<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>
<DatePicker
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>
<DatePicker
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 -->
</section>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/Timeframes
* Timeframe filter with date range pickers and
* duration range selectors.
*/
import { ref, computed, watch, onMounted, onBeforeUnmount } 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 allMapDataStore = useAllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
const selectRange = ref(1000); // Number of divisions for the selection range
const selectArea = ref(null);
const chart = ref(null);
const canvasId = ref(null);
const startTime = ref(null);
const endTime = ref(null);
const startMinDate = ref(null);
const startMaxDate = ref(null);
const endMinDate = ref(null);
const endMaxDate = ref(null);
const panelProps = ref({
onClick: (event) => {
event.stopPropagation();
},
});
// user select time start and end
const timeFrameStartEnd = computed(() => {
if (!startTime.value || !endTime.value) {
return [];
}
const start = getMoment(startTime.value).format("YYYY-MM-DDTHH:mm:00");
const end = getMoment(endTime.value).format("YYYY-MM-DDTHH:mm:00");
return [start, end];
});
watch(timeFrameStartEnd, (newValue) => {
if (newValue.length === 2) {
selectTimeFrame.value = newValue; // Data to send to the backend
}
});
// Compute slider data; time format: millisecond timestamps
const sliderData = computed(() => {
const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
const range = xAxisMax - xAxisMin;
const step = range / selectRange.value;
const data = [];
for (let i = 0; i <= selectRange.value; i++) {
data.push(xAxisMin + step * i);
}
return data;
});
// Add the minimum and maximum values
const timeFrameData = computed(() => {
if (!filterTimeframe.value?.data || filterTimeframe.value.data.length < 10)
return [];
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)
// Minimum: (f - b)(c - a) = (e - a)(d - b), solve for b = (ed - ad - fa - fc) / (e - c - a)
// Maximum: (f - b)(e - c) = (f - d)(e - a), solve for f = (be - bc - de + da) / (a - c)
// Y-axis minimum value
const a = 0;
let b;
const c = 1;
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 = 0;
}
// Y-axis maximum value
const ma = 9;
const mb = filterTimeframe.value.data[8].y;
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) {
mf = 0;
}
// Add the minimum value
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;
});
const labelsData = computed(() => {
const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
const numPoints = 11;
const step = (max - min) / (numPoints - 1);
const data = [];
for (let i = 0; i < numPoints; i++) {
const x = min + i * step;
data.push(x);
}
return data;
});
watch(selectTimeFrame, (newValue, oldValue) => {
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];
resizeMask(chart.value);
}
});
/**
* Adjusts the overlay mask size.
* @param {object} chartInstance - The Chart.js instance.
*/
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") {
resizeLeftMask(chartInstance, from);
resizeRightMask(chartInstance, to);
}
}
/**
* Adjusts the left overlay mask size.
* @param {object} chartInstance - The Chart.js instance.
*/
function resizeLeftMask(chartInstance, from) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-left");
if (!canvas || !mask) return;
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left}px`;
mask.style.width = `${chartInstance.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chartInstance.chartArea.height}px`;
}
/**
* Adjusts the right overlay mask size.
* @param {object} chartInstance - The Chart.js instance.
*/
function resizeRightMask(chartInstance, to) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-right");
if (!canvas || !mask) return;
mask.style.left = `${canvas.offsetLeft + chartInstance.chartArea.left + chartInstance.chartArea.width * to}px`;
mask.style.width = `${chartInstance.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chartInstance.chartArea.top}px`;
mask.style.height = `${chartInstance.chartArea.height}px`;
}
/**
* Creates and renders the Chart.js area chart for timeframe data.
*/
function createChart() {
const max = filterTimeframe.value.y_axis.max * 1.1;
const minX = timeFrameData.value[0]?.x;
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
const data = {
labels: labelsData.value,
datasets: [
{
label: "Case",
data: timeFrameData.value,
fill: "start",
showLine: false,
tension: 0.4,
backgroundColor: "rgba(0,153,255)",
pointRadius: 0,
x: "x",
y: "y",
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
legend: false, // Hide legend
filler: {
propagate: false,
},
title: false,
},
// animations: false, // Disable animations
animation: {
onComplete: (e) => {
resizeMask(e.chart);
},
},
interaction: {
intersect: true,
},
scales: {
x: {
type: "time",
min: minX,
max: maxX,
ticks: {
autoSkip: true,
maxRotation: 0, // Do not rotate labels (0~50)
color: "#334155",
display: true,
source: "labels",
},
grid: {
display: false, // Hide x-axis grid lines
},
time: {
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
display: false, // Hide values, only show grid lines
stepSize: max / 4,
},
grid: {
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
},
},
},
};
const config = {
type: "line",
data: data,
options: options,
};
canvasId.value = document.getElementById("chartCanvasId");
chart.value = new Chart(canvasId.value, config);
}
/**
* Handles slider value changes.
* @param {array} e [1, 100]
*/
function changeSelectArea(e) {
// When the calendar changes, the slider follows
const sliderDataVal = sliderData.value;
const start = sliderDataVal[e[0].toFixed()];
const end = sliderDataVal[e[1].toFixed()]; // Get the index, which must be an integer.
startTime.value = new Date(start);
endTime.value = new Date(end);
// Reset the start/end calendar selection range
endMinDate.value = new Date(start);
startMaxDate.value = new Date(end);
// Recalculate the chart mask
resizeMask(chart.value);
}
/**
* Updates the slider and chart when a start or end time is selected.
* @param {object} e - The selected date object.
* @param {string} direction start or end
*/
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) => {
let closestIndex = 0;
closestIndex =
((target - sliderDataVal[0]) /
(sliderDataVal[sliderDataVal.length - 1] - sliderDataVal[0])) *
sliderDataVal.length;
let result = Math.round(Math.abs(closestIndex));
result = Math.min(result, selectRange.value);
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;
// Recalculate the chart mask
if (!Number.isNaN(closestIndexes[0]) && !Number.isNaN(closestIndexes[1]))
resizeMask(chart.value);
else return;
}
onMounted(() => {
// Chart.js
Chart.register(...registerables);
createChart();
// Slider
selectArea.value = [0, selectRange.value];
// Calendar
startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
// Set the calendar range to match the timeline range
startTime.value = startMinDate.value;
endTime.value = startMaxDate.value;
});
onBeforeUnmount(() => {
chart.value?.destroy();
chart.value = null;
});
</script>

View File

@@ -0,0 +1,503 @@
<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"
>
<!-- Range Selection -->
<section class="py-2 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">
<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"
/>
<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"
/>
</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.
</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 w-full">
<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>
</tr>
</thead>
<tbody>
<tr
v-for="(trace, key) in traceList"
:key="key"
class="cursor-pointer hover:text-primary"
@click="switchCaseData(trace.id, trace.base_count)"
>
<td class="p-2 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-full bg-primary" :style="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>
</section>
<!-- Trace item Table -->
<section v-show="showTraceId" class="pl-4 h-full min-w-full py-2 space-y-2">
<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>
</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 in columnData" :key="col.field">
<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>
</template>
</Column>
</div>
</DataTable>
</div>
</section>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/Filter/Trace
* Trace filter with trace selection table and
* trace detail display.
*/
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useLoadingStore } from "@/stores/loading";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
import { cloneDeep } from "lodash-es";
const emit = defineEmits(["filter-trace-selectArea"]);
const allMapDataStore = useAllMapDataStore();
const loadingStore = useLoadingStore();
const {
infinit404,
baseInfiniteStart,
baseTraces,
baseTraceTaskSeq,
baseCases,
} = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore);
const processMap = ref({
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
const infiniteData = ref([]);
const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const chartOptions = ref(null);
const selectArea = ref([0, 1]);
const cyTraceRef = ref(null);
const cyTraceInstance = ref(null);
const traceTotal = computed(() => {
return baseTraces.value.length;
});
defineExpose({ selectArea, showTraceId, traceTotal });
const traceCountTotal = computed(() => {
return baseTraces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
});
const traceList = computed(() => {
if (traceCountTotal.value === 0) return [];
return baseTraces.value
.map((trace) => {
return {
id: trace.id,
value: progressWidth(
Number(((trace.count / traceCountTotal.value) * 100).toFixed(1)),
),
count: trace.count.toLocaleString("en-US"),
base_count: trace.count,
ratio: getPercentLabel(trace.count / traceCountTotal.value),
};
})
.slice(selectArea.value[0], selectArea.value[1]);
});
const caseTotalPercent = computed(() => {
if (traceCountTotal.value === 0) return "0%";
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)",
);
return {
// Data to display
labels,
datasets: [
{
label: "Trace", // Dataset label
data,
backgroundColor: selectAreaData,
categoryPercentage: 1,
barPercentage: 1,
},
],
};
});
const caseData = computed(() => {
const data = cloneDeep(infiniteData.value);
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 = cloneDeep(baseCases.value);
let result = [
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
];
if (data.length !== 0) {
result = [
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...(data[0]?.attributes ?? []).map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
}
return result;
});
watch(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
});
watch(infinit404, (newValue) => {
if (newValue === 404) infinitMaxItems.value = true;
});
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && isScrollTop.scrollTop !== undefined)
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
/**
* Set bar chart Options
*/
function barOptions() {
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
},
},
plugins: {
legend: {
// Legend
display: false,
},
tooltip: {
callbacks: {
label: (tooltipItems) => {
return `${tooltipItems.dataset.label}: ${tooltipItems.parsed.y}%`;
},
},
},
},
animations: false,
scales: {
x: {
display: false,
},
y: {
ticks: {
// Set tick intervals
display: false, // Hide values, only show grid lines
min: 0,
max: traceList.value[0]?.ratio ?? 1,
stepSize: (traceList.value[0]?.ratio ?? 1) / 4,
},
grid: {
color: "rgba(100,116,139)",
z: 1,
},
border: {
display: false, // Hide the extra border line on the left
},
},
},
};
}
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val) {
if (Number((val * 100).toFixed(1)) >= 100) return 100;
else return Number.parseFloat((val * 100).toFixed(1));
}
/**
* set progress bar width
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* switch case data
* @param {number} id case id
* @param {number} count - The total number of cases.
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
if (id === showTraceId.value) return;
isLoading.value = true; // Always show loading screen
try {
infinit404.value = null;
infinitMaxItems.value = false;
baseInfiniteStart.value = 0;
allMapDataStore.baseTraceId = id;
infiniteData.value = await allMapDataStore.getBaseTraceDetail();
showTraceId.value = id; // Set after getDetail so the case table finishes loading before switching showTraceId
createCy();
} catch (error) {
console.error("Failed to load data:", error);
} finally {
isLoading.value = false;
}
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
baseTraceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100,
},
});
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData() {
processMap.value.edges = [];
baseTraceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
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() {
const graphId = cyTraceRef.value;
setNodesData();
setEdgesData();
cyTraceInstance.value?.destroy();
cyTraceInstance.value = cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
}
/**
* Infinite scroll: listens for scroll reaching the bottom.
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if (
infinitMaxItems.value ||
baseCases.value.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
/**
* Infinite scroll: loads more data when the bottom is reached.
*/
async function fetchData() {
try {
isLoading.value = true;
infiniteFinish.value = false;
baseInfiniteStart.value += 20;
await allMapDataStore.getBaseTraceDetail();
infiniteData.value = [...infiniteData.value, ...baseCases.value];
infiniteFinish.value = true;
isLoading.value = false;
} catch (error) {
console.error("Failed to load data:", error);
infiniteFinish.value = true;
isLoading.value = false;
}
}
onMounted(() => {
isLoading.value = true; // Will be closed after createCy finishes
setNodesData();
setEdgesData();
createCy();
chartOptions.value = barOptions();
selectArea.value = [0, traceTotal.value];
isLoading.value = false;
});
onBeforeUnmount(() => {
cyTraceInstance.value?.destroy();
});
</script>
<style scoped>
@reference "../../../../assets/tailwind.css";
/* Table set */
:deep(.p-datatable-thead) {
@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;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
max-width: 184px;
overflow-wrap: break-word;
word-wrap: break-word;
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,394 @@
<template>
<Drawer
: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>
<div class="pt-4 h-full flex items-center justify-start">
<!-- Trace List -->
<section class="w-80 h-full pr-4 border-r border-neutral-300">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">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-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th
class="h2 px-2 border-b border-neutral-500 text-start"
colspan="3"
>
Occurrences
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(trace, key) in traceList"
:key="key"
class="cursor-pointer hover:text-primary"
@click="switchCaseData(trace.id, trace.base_count)"
>
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div
class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden"
>
<div class="h-full bg-primary" :style="trace.value"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Trace item Table -->
<section class="pl-4 h-full w-[calc(100%_-_320px)]">
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-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>
</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 v-for="col in columnData" :key="col.field">
<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>
</template>
</Column>
</div>
</DataTable>
</div>
</section>
</div>
</Drawer>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/SidebarTraces
* Traces sidebar showing path insights with
* clickable trace lists for highlighting on the map.
*/
import { ref, computed, watch, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useLoadingStore } from "@/stores/loading";
import { useAllMapDataStore } from "@/stores/allMapData";
import cytoscapeMapTrace from "@/module/cytoscapeMapTrace.js";
import { cloneDeep } from "lodash-es";
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 processMap = ref({
nodes: [],
edges: [],
});
const showTraceId = ref(null);
const infinitMaxItems = ref(false);
const infiniteData = ref([]);
const infiniteFinish = ref(true); // Whether infinite scroll loading is complete
const cyTraceRef = ref(null);
const cyTraceInstance = ref(null);
const traceTotal = computed(() => {
return traces.value.length;
});
const traceList = computed(() => {
const sum = traces.value
.map((trace) => trace.count)
.reduce((acc, cur) => acc + cur, 0);
if (sum === 0) return [];
const result = traces.value.map((trace) => {
return {
id: trace.id,
value: progressWidth(Number(((trace.count / sum) * 100).toFixed(1))),
count: trace.count.toLocaleString("en-US"),
base_count: trace.count,
ratio: getPercentLabel(trace.count / sum),
};
});
return result;
});
const caseData = computed(() => {
const data = cloneDeep(infiniteData.value);
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 = cloneDeep(props.cases);
let result = [
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
];
if (data.length !== 0) {
result = [
{ field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" },
{ field: "completed_at", header: "End time" },
...(data[0]?.attributes ?? []).map((att, index) => ({
field: `att_${index}`,
header: att.key,
})),
];
}
return result;
});
watch(infinit404, (newValue) => {
if (newValue === 404) infinitMaxItems.value = true;
});
watch(
traceId,
(newValue) => {
showTraceId.value = newValue;
},
{ immediate: true },
);
watch(showTraceId, (newValue, oldValue) => {
const isScrollTop = document.querySelector(".infiniteTable");
if (isScrollTop && isScrollTop.scrollTop !== undefined)
if (newValue !== oldValue) isScrollTop.scrollTop = 0;
});
watch(infiniteFirstCases, (newValue) => {
if (infiniteFirstCases.value)
infiniteData.value = cloneDeep(newValue);
});
/**
* Number to percentage
* @param {number} val - The raw ratio value.
* @returns {string} The formatted percentage string.
*/
function getPercentLabel(val) {
if (Number((val * 100).toFixed(1)) >= 100) return `100%`;
else return `${(val * 100).toFixed(1)}%`;
}
/**
* set progress bar width
* @param {number} value - The percentage value.
* @returns {string} The CSS width style string.
*/
function progressWidth(value) {
return `width:${value}%;`;
}
/**
* switch case data
* @param {number} id case id
* @param {number} count - The total number of cases.
*/
async function switchCaseData(id, count) {
// Do nothing if clicking the same id
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
}
/**
* Assembles the trace element nodes data for Cytoscape rendering.
*/
function setNodesData() {
// Clear nodes to prevent accumulation on each render
processMap.value.nodes = [];
// Populate nodes with data returned from the API call
traceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({
data: {
id: index,
label: node,
backgroundColor: "#CCE5FF",
bordercolor: "#003366",
shape: "round-rectangle",
height: 80,
width: 100,
},
});
});
}
/**
* Assembles the trace edge line data for Cytoscape rendering.
*/
function setEdgesData() {
processMap.value.edges = [];
traceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
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() {
const graphId = cyTraceRef.value;
setNodesData();
setEdgesData();
cyTraceInstance.value?.destroy();
cyTraceInstance.value = cytoscapeMapTrace(processMap.value.nodes, processMap.value.edges, graphId);
}
/**
* create map
*/
async function show() {
isLoading.value = true; // Will be closed after createCy finishes
// Reset to the first trace id when sidebar closes, due to trace API dependency
showTraceId.value = traces.value[0]?.id;
infiniteStart.value = 0;
setNodesData();
setEdgesData();
createCy();
isLoading.value = false;
}
/**
* Infinite scroll: listens for scroll reaching the bottom.
* @param {Event} event - The scroll event.
*/
function handleScroll(event) {
if (
infinitMaxItems.value ||
props.cases.length < 20 ||
infiniteFinish.value === false
)
return;
const container = event.target;
const overScrollHeight =
container.scrollTop + container.clientHeight >= container.scrollHeight;
if (overScrollHeight) fetchData();
}
/**
* Infinite scroll: loads more data when the bottom is reached.
*/
async function fetchData() {
try {
isLoading.value = true;
infiniteFinish.value = false;
infiniteStart.value += 20;
await allMapDataStore.getTraceDetail();
infiniteData.value = [...infiniteData.value, ...props.cases];
infiniteFinish.value = true;
isLoading.value = false;
} catch (error) {
console.error("Failed to load data:", error);
infiniteFinish.value = true;
isLoading.value = false;
}
}
onBeforeUnmount(() => {
cyTraceInstance.value?.destroy();
});
</script>
<style scoped>
@reference "../../../assets/tailwind.css";
/* Progress bar color */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary;
}
/* Table set */
:deep(.p-datatable-thead) {
@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;
}
:deep(.p-datatable.p-datatable-gridlines .p-datatable-tbody > tr > td) {
min-width: 72px;
max-width: 184px;
overflow-wrap: break-word;
word-wrap: break-word;
}
/* Center datatable header */
:deep(.p-column-header-content) {
@apply justify-center;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<Drawer
:visible="sidebarView"
:closeIcon="'pi pi-chevron-left'"
:modal="false"
position="left"
:dismissable="false"
>
<template #header>
<p class="h1">Visualization Setting</p>
</template>
<div>
<!-- View -->
<div class="my-4 border-b border-neutral-200">
<p class="h2">View</p>
<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()"
>
Process Map
</span>
<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')"
>
Curved
</span>
<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')"
>
Horizontal
</span>
<span
class="btn-toggle-item"
:class="rank === 'TB' ? 'btn-toggle-show' : ''"
@click="switchRank('TB')"
>
Vertical
</span>
</li>
</ul>
</div>
<!-- Data Layer -->
<div>
<p class="h2">Data Layer</p>
<ul class="space-y-2">
<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')"
/>
<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>
</div>
</li>
<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')"
/>
<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>
</div>
</li>
</ul>
</div>
</div>
</Drawer>
</template>
<script setup>
// The Lucia project.
// Copyright 2023-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/Map/SidebarView Visualization
* settings sidebar for map view type (process/BPMN), curve
* style, direction, and data layer selection.
*/
import { ref, onMounted } from "vue";
import { storeToRefs } from "pinia";
import { useMapPathStore } from "@/stores/mapPathStore";
defineProps({
sidebarView: {
type: Boolean,
require: true,
},
});
const emit = defineEmits([
"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 },
]);
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 },
]);
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
/**
* Switches the map type and emits the change event.
* @param {string} type - 'processMap' or 'bpmn'.
*/
function switchMapType(type) {
mapType.value = type;
emit("switch-map-type", mapType.value);
}
/**
* Switches the curve style and emits the change event.
* @param {string} style - 'unbundled-bezier' (curved) or 'taxi' (elbow).
*/
function switchCurveStyles(style) {
curveStyle.value = style;
emit("switch-curve-styles", curveStyle.value);
}
/**
* Switches the graph layout direction and emits the change event.
* @param {string} rankValue - 'TB' (vertical) or 'LR' (horizontal).
*/
function switchRank(rankValue) {
rank.value = rankValue;
emit("switch-rank", rank.value);
}
/**
* Switches the data layer type (frequency or duration) and option.
* @param {Event} e - The change event from the radio or select.
* @param {string} type - 'freq' or 'duration'.
*/
function switchDataLayerType(e, type) {
let value = "";
if (e.target.value !== "freq" && e.target.value !== "duration")
value = e.target.value;
switch (type) {
case "freq":
value = value || selectedFreq.value || "total";
dataLayerType.value = type;
dataLayerOption.value = value;
selectedFreq.value = value;
break;
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);
}
/** Switches to Process Map view. */
function onProcessMapClick() {
mapPathStore.setIsBPMNOn(false);
switchMapType("processMap");
}
/** Switches to BPMN Model view. */
function onBPMNClick() {
mapPathStore.setIsBPMNOn(true);
switchMapType("bpmn");
}
onMounted(() => {
dataLayerType.value = "freq";
dataLayerOption.value = "total";
});
</script>

View File

@@ -0,0 +1,245 @@
<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>
</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>
<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>
</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>
<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>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
/**
* @module components/Discover/StatusBar Collapsible status bar
* showing dataset statistics (cases, traces, activities,
* 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";
const route = useRoute();
const allMapDataStore = useAllMapDataStore();
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
const isPanel = ref(false);
const statData = ref(null);
/**
* Converts a ratio (01) to a percentage number (0100), capped at 100.
* @param {number} val - The ratio value to convert.
* @returns {number} The percentage value.
*/
function getPercentLabel(val) {
if (Number((val * 100).toFixed(1)) >= 100) return 100;
else return Number.parseFloat((val * 100).toFixed(1));
}
/** Transforms raw stats into display-ready format with localized numbers and time labels. */
function getStatData() {
if (!stats.value) return;
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),
},
traces: {
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),
},
tasks: {
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",
),
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");
switch (params.type) {
case "log":
logId.value = isCheckPage ? file.parent?.id : params.fileId;
break;
case "filter":
createFilterId.value = isCheckPage ? file.parent?.id : params.fileId;
break;
}
await allMapDataStore.getAllMapData();
await getStatData();
isPanel.value = false; // Collapsed by default
});
</script>
<style scoped>
@reference "../../assets/tailwind.css";
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-neutral-900;
}
</style>

View File

@@ -1,513 +0,0 @@
<template>
<Sidebar :visible="sidebarFilter" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" :baseZIndex="15" class="!w-11/12 !bg-neutral-100">
<template #header>
<ul class="flex space-x-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('filter')" :class="tab === 'filter'? 'text-neutral-900': 'text-neutral-500'">Filter</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('funnel')" :class="tab === 'funnel'? 'text-neutral-900': 'text-neutral-500'">Funnel</li>
</ul>
</template>
<!-- header: filter -->
<div v-if="tab === 'filter'" class="pt-4 bg-neutral-100 flex w-full h-full">
<!-- title: filter silect -->
<div class="space-y-2 mr-4 w-56 text-sm">
<div>
<p class="h2">Filter Type</p>
<div v-for="(item, index) in selectFilter['Filter Type']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[0]" :inputId="item + index" name="Filter Type" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence'">
<p class="h2">Activity Sequence</p>
<div v-for="(item, index) in selectFilter['Activity Sequence']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[1]" :inputId="item + index" name="Activity Sequence" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[1] === 'Start activity & end activity'">
<p class="h2">Start & End</p>
<div v-for="(item, index) in selectFilter['Start & End']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[2]" :inputId="item + index" name="Start & End" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['Mode']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[3]" :inputId="item + index" name="Mode" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Attributes'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['ModeAtt']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[4]" :inputId="item + index" name="ModeAtt" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div>
<p class="h2">Refine</p>
<div v-for="(item, index) in selectFilter['Refine']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[5]" :inputId="item + index" name="Refinee" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Timeframes'">
<p class="h2">Containment</p>
<div v-for="(item, index) in selectFilter['Containment']" :key="index" class="flex align-items-center">
<RadioButton v-model="selectValue[6]" :inputId="item + index" name="Containment" :value="item" />
<label :for="item + index" class="ml-2">{{ item }}</label>
</div>
</div>
</div>
<!-- title: Activity Select -->
<div class="space-y-2 w-[calc(100%_-_240px)] h-[calc(100%_-_106px)]">
<p class="h2 ml-1">Activity Select</p>
<!-- Filter task Data-->
<ActOccCase v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Have activity(s)'" :tableTitle="'Activity List'" :tableData="filterTaskData" :tableSelect="selectFilterTask" :progressWidth ="progressWidth" @on-row-select="onRowAct"></ActOccCase>
<!-- Filter Start Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'Start'" :tableTitle="'Start activity'" :tableData="filterStartData" :tableSelect="selectFilterStart" :progressWidth ="progressWidth" @on-row-select="onRowStart"></ActOcc>
<!-- Filter End Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'End'" :tableTitle="'End activity'" :tableData="filterEndData" :tableSelect="selectFilterEnd" :progressWidth ="progressWidth" @on-row-select="onRowEnd"></ActOcc>
<!-- Filter Start And End Data -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity & end activity' && selectValue[2] === 'Start & End'" class="flex justify-between items-center w-full h-full space-x-4 ">
<ActOcc :tableTitle="'Start activity'" :tableData="filterStartToEndData" :tableSelect="selectFilterStartToEnd" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="startRow"></ActOcc>
<ActOcc :tableTitle="'End activity'" :tableData="filterEndToStartData" :tableSelect="selectFilterEndToStart" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="endRow"></ActOcc>
</div>
<!-- Filter Sequence -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'" class="flex justify-between items-center w-full h-full space-x-4">
<ActAndSeq :filterTaskData="filterTaskData" :progressWidth ="progressWidth" :listSeq="listSeq" @update:listSeq="onUpdateListSeq"></ActAndSeq>
</div>
<!-- Button -->
<div class="float-right space-x-4 px-4 py-2">
<button class="btn btn-sm btn-neutral" @click="reset">Clear</button>
<button class="btn btn-sm btn-neutral" @click="submit">Apply</button>
</div>
</div>
</div>
<!-- header: funnel -->
<div v-if="tab === 'funnel'" class="bg-neutral-10 w-full h-full">
<div class="h-[calc(100%_-_58px)] border-b border-neutral-300 mb-2">
<div v-if="temporaryData.length === 0" class="h-full flex justify-center items-center">
<span class="text-neutral-500">No Filter.</span>
</div>
<div v-else>
temporaryData:{{ temporaryData }}
<Timeline :value="events">
<template #content="slotProps">
{{ slotProps.item.status }}
</template>
</Timeline>
<!-- <Timeline :value="events" align="alternate" class="customized-timeline">
<template #marker="slotProps">
<span class="!flex !w-2rem !h-2rem !align-items-center !justify-content-center !text-white !border-circle !z-1 !shadow-1" :style="{ backgroundColor: slotProps.item.color }">
<i :class="slotProps.item.icon"></i>
</span>
</template>
<template #content="slotProps">
{{ slotProps.item.status }}
<Card>
<template #title>
{{ slotProps.item.status }}
</template>
<template #subtitle>
{{ slotProps.item.date }}
</template>
<template #content>
<img v-if="slotProps.item.image" :src="`https://primefaces.org/cdn/primevue/images/product/${slotProps.item.image}`" :alt="slotProps.item.name" width="200" class="shadow-1" />
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa ratione quam perferendis esse, cupiditate
neque quas!
</p>
<Button label="Read more" text></Button>
</template>
</Card>
</template>
</Timeline> -->
</div>
</div>
<!-- Button -->
<div class="">
<div class="float-right space-x-4 px-4 py-2">
<button class="btn btn-sm btn-neutral" @click="deleteAll">Delete All</button>
<button class="btn btn-sm btn-neutral" @click="submitAll">Apply All</button>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
import ActOccCase from '@/components/Discover/table/actOccCase.vue';
import ActOcc from '@/components/Discover/table/actOcc.vue';
import ActAndSeq from '@/components/Discover/table/actAndSeq.vue';
export default {
props: {
sidebarFilter: {
type: Boolean,
require: true,
},
filterTasks: {
type: Array,
require: true,
},
filterStartToEnd: {
type: Array,
require: true,
},
filterEndToStart: {
type: Array,
require: true,
},
filterTimeframe: {
type: Object,
require: true,
},
filterTrace: {
type: Array,
require: true,
},
},
setup() {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { tempFilterId } = storeToRefs(allMapDataStore);
return { isLoading, tempFilterId, allMapDataStore }
},
data() {
return {
events: [
{ status: 'Ordered', date: '15/10/2020 10:30', icon: 'pi pi-shopping-cart', color: '#9C27B0'},
{ status: 'Processing', date: '15/10/2020 14:00', icon: 'pi pi-cog', color: '#673AB7' },
{ status: 'Shipped', date: '15/10/2020 16:15', icon: 'pi pi-shopping-cart', color: '#FF9800' },
{ status: 'Delivered', date: '16/10/2020 10:00', icon: 'pi pi-check', color: '#607D8B' }
],
selectFilter: {
'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'],
'Activity Sequence':['Have activity(s)', 'Start activity & end activity', 'Sequence'],
'Start & End': ['Start', 'End', 'Start & End'],
'Mode': ['Directly follows', 'Eventually follows'],
'ModeAtt': ['Case', 'Activity'],
'Refine': ['Include', 'Exclude'],
'Containment': ['Contained in', 'Started in', 'End in', 'Activity in', 'Trim'],
},
tab: 'filter', // filter | funnel
// selectValue: ['Sequence', 'Have activity(s)', 'Start', 'Directly follows', 'Case', 'Include', 'Contained in'],
selectValue: {
0: 'Sequence',
1: 'Have activity(s)',
2: 'Start',
3: 'Directly follows',
4: 'Case',
5: 'Include',
6: 'Contained in',
},
selectFilterTask: null,
selectFilterStart: null,
selectFilterEnd: null,
selectFilterStartToEnd: null,
selectFilterEndToStart: null,
listSeq: [],
//若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
isStartSelected: null,
isEndSelected: null,
isActAllTask: true,
rowData: [],
temporaryData: [],
}
},
components: {
ActOccCase,
ActOcc,
ActAndSeq,
},
computed: {
// All Task
filterTaskData: function() {
return this.isActAllTask? this.setHaveAct(this.filterTasks) : this.filterTaskData;
},
// Start and End Task
filterStartData: function() {
return this.setActData(this.filterStartToEnd);
},
filterEndData: function() {
return this.setActData(this.filterEndToStart);
},
filterStartToEndData: function() {
return this.isEndSelected ? this.setStartAndEndData(this.filterEndToStart, this.rowData, 'sources') : this.setActData(this.filterStartToEnd);
},
filterEndToStartData: function() {
return this.isStartSelected ? this.setStartAndEndData(this.filterStartToEnd, this.rowData, 'sinks') : this.setActData(this.filterEndToStart);
}
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? `${val * 100}%` : `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
//設定 Have activity(s) 內容
/**
* @param {array} data filterTaskData
*/
setHaveAct(data){
return data.map(task => {
return {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
case_value: Number(task.case_ratio * 100),
cases: task.cases.toLocaleString('en-US'),
case_ratio: this.getPercentLabel(task.case_ratio),
};
}).sort((x, y) => y.occurrences - x.occurrences);
},
// 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容
/**
* @param {array} array filterStartToEnd / filterEndToStart
*/
setActData(array) {
let list = [];
array.forEach((task, index) => {
let data = {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
};
list.push(data);
});
return list;
},
/**
* @param {array} select select Have activity(s) rows
*/
onRowAct(select){
this.selectFilterTask = select;
},
/**
* @param {object} e select Start rows
*/
onRowStart(e){
this.selectFilterStart = e.data;
},
/**
* @param {object} e select End rows
*/
onRowEnd(e){
this.selectFilterEnd = e.data;
},
/**
* @param {array} e Update List Seq
*/
onUpdateListSeq(listSeq) {
this.listSeq = listSeq;
this.isActAllTask = false;
},
// 在 Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
/**
* @param {object} e object contains selected row's data
*/
startRow(e){
this.selectFilterStartToEnd = e.data;
if(this.isStartSelected === null || this.isStartSelected === true){
this.isStartSelected = true;
this.isEndSelected = false;
this.rowData = e.data;
}
},
endRow(e) {
this.selectFilterEndToStart = e.data;
if(this.isEndSelected === null || this.isEndSelected === true){
this.isEndSelected = true;
this.isStartSelected = false;
this.rowData = e.data;
}
},
// 重新設定連動的 filterStartToEndData / filterEndToStartData 內容
/**
* @param {array} eventData Start or End List
* @param {object} rowData 所選擇的 row's data
* @param {string} event sinks / sources
*/
setStartAndEndData(eventData, rowData, event){
const filterData = event === 'sinks' ? this.filterEndToStart : this.filterStartToEnd;
const relatedItems = eventData
.find(task => task.label === rowData.label)
?.[event]?.filter(item => filterData.some(ele => ele.label === item))
?.map(item => filterData.find(ele => ele.label === item));
if (!relatedItems) return [];
return relatedItems.map(item => ({
label: item.label,
occ_value: Number(item.occurrence_ratio * 100),
occurrences: Number(item.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(item.occurrence_ratio),
}));
},
/**
* 清空選項
*/
reset(massage) {
this.selectFilterTask = null;
this.selectFilterStart = null;
this.selectFilterEnd = null;
this.selectFilterStartToEnd = null;
this.selectFilterEndToStart = null;
this.listSeq = [];
this.isStartSelected = null;
this.isEndSelected = null;
this.isActAllTask = true;
massage ? this.$toast.success('Reset Success.') : null;
},
// header:Filter 發送選取的資料
submit(){
let data;
let sele = this.selectValue;
let isExclude = sele[5] === 'Exclude' ? true : false
// Filter Type 選 Sequence 的行為
// 若陣列為空,則跳出警告訊息
if(sele[0] === 'Sequence'){
if(sele[1] === 'Have activity(s)'){ // Activity Sequence 選 Have activity(s) 的行為
if(this.selectFilterTask === null || this.selectFilterTask.length === 0) return this.$toast.error('Not selected');
else {
// 將多選的 task 拆成一包包 obj
data = this.selectFilterTask.map(task => {
return {
type : 'contains-task',
task : task.label,
is_exclude : isExclude,
}
})
};
}else if(sele[1] === 'Start activity & end activity') { // Activity Sequence 選 Start activity & end activity 的行為
if(sele[2] === 'Start') {
if(this.selectFilterStart === null || this.selectFilterStart.length === 0) return this.$toast.error('Not selected');
else {
data = {
type: 'starts-with',
task: this.selectFilterStart.label,
is_exclude: isExclude,
}
};
}else if(sele[2] === 'End') {
if(this.selectFilterEnd === null || this.selectFilterEnd.length === 0) return this.$toast.error('Not selected');
else {
data = {
type: 'starts-with',
task: this.selectFilterEnd.label,
is_exclude: isExclude,
}
};
}else if(sele[2] === 'Start & End') {
if(this.selectFilterStartToEnd === null || this.selectFilterStartToEnd.length === 0 || this.selectFilterEndToStart === null || this.selectFilterEndToStart.length === 0 ) return this.$toast.error('Both Start and End must be selected.');
else {
data = {
type: 'start-end',
starts_with: this.selectFilterStartToEnd.label,
ends_with: this.selectFilterEndToStart.label,
is_exclude: isExclude,
}
}
};
}else if(sele[1] === 'Sequence'){ // Activity Sequence 選 Sequence 的行為
if(this.listSeq.length < 2) return this.$toast.error('Select two or more.');
else {
data = {
type: sele[3] === 'Directly follows' ? 'directly-follows' : 'eventually-follows',
task_seq: this.listSeq.map(task => task.label),
is_exclude: isExclude,
}
};
}
}
// 將資料指向 Vue data 雙向綁定
const postData = Array.isArray(data) ? data : [data];
this.temporaryData.push(...postData);
// 結束後要清空資料
this.reset(false);
// 發送時isLoading
this.isLoading = true;
// 結束後,要跳出傳送成功的訊息
// 跳轉 this.tab = 'funnel';
setTimeout(() => {
this.isLoading = false;
this.$toast.success('Filter Success. Click on Funnel to view the configuration result.');
}, 1000);
// 快速檢查每一 filter 規則是否為空集合
let logId = this.$route.params.logId;
this.axios.post(`/api/filters/has-result?log_id=${logId}`, postData)
.then(res=>{
res.data.result ? null : this.$toast.warning('No result.');
}).catch(err=>{
})
},
// header:Funnel 刪除全部的 Funnel
deleteAll() {
this.temporaryData = [];
this.$toast.success('All deleted.');
},
// header:Funnel 發送暫存的選取資料
submitAll() {
let logId = this.$route.params.logId;
this.axios.post(`/api/temp-filters?log_id=${logId}`, this.temporaryData)
.then(res=>{
console.log(res);
this.tempFilterId = res.data.id;
}).catch(err=>{
console.log(err);
})
}
},
}
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{
appearance: none;
}
/* TimeLine */
:deep(.p-timeline-event-marker) {
@apply !bg-primary !border-primary !h-2 !w-2
}
:deep(.p-timeline-event-connector) {
@apply !bg-primary my-2 !w-[1px]
}
</style>

View File

@@ -1,266 +0,0 @@
<template>
<Sidebar :visible="sidebarState" :closeIcon="'pi pi-angle-right'" :modal="false" position="right" :dismissable="true" class="!w-[360px]" @hide="hide" @show="show">
<template #header>
<ul class="flex space-x-4 pl-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('summary')" :class="tab === 'summary'? 'text-neutral-900': ''">Summary</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('insight')" :class="tab === 'insight'? 'text-neutral-900': ''">Insight</li>
</ul>
</template>
<!-- header: summary -->
<div v-if="tab === 'summary'">
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.cases.count) }} / {{ numberLabel(stats.cases.total) }}</span>
<ProgressBar :value="valueCases" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.cases.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.traces.count) }} / {{ numberLabel(stats.traces.total) }}</span>
<ProgressBar :value="valueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.traces.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.task_instances.count) }} / {{ numberLabel(stats.task_instances.total) }}</span>
<ProgressBar :value="valueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.task_instances.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.tasks.count) }} / {{ numberLabel(stats.tasks.total) }}</span>
<ProgressBar :value="valueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.tasks.ratio) }}</span>
</div>
</li>
</ul>
<!-- Log Timeframe -->
<div class="pt-1 pb-4 border-b border-neutral-300">
<p class="h2">Log Timeframe</p>
<p><span class="px-4">{{ moment(stats.started_at
) }}</span>~<span class="px-4">{{ moment(stats.completed_at
) }}</span></p>
</div>
<!-- Case Duration -->
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.min) }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.average
) }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.median) }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.max) }}</li>
</ul>
</div>
</div>
<!-- header: insight -->
<div v-if="tab === 'insight'">
<div class="border-b-2 border-neutral-300 mb-4">
<p class="h2">Most frequent</p>
<ul class="list-disc ml-6">
<li>Activity:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_tasks" :key="key">{{ value }}<span v-if="key !== insights.most_freq_tasks.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Inbound connections:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_in" :key="key">{{ value }}<span v-if="key !== insights.most_freq_in.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Outbound connections:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_out" :key="key">{{ value }}<span v-if="key !== insights.most_freq_out.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
</ul>
<p class="h2">Most time-consuming</p>
<ul class="list-disc ml-6 mb-4">
<li class="w-full">Activity:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_time_tasks" :key="key">{{ value }}<span v-if="key !== insights.most_time_tasks.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Connection:&nbsp;
<span class="text-primary break-words" v-for="(item, key) in insights.most_time_edges" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub ">arrow_forward</span>&nbsp;</span>
</span><span v-if="key !== insights.most_time_edges.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
</ul>
</div>
<div>
<ul class="text-neutral-500 grid grid-cols-2 gap-2 text-center text-sm font-medium mb-2">
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 0" :class="active1 === 0? 'text-primary border-primary':''">Self-loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 1" :class="active1 === 1? 'text-primary border-primary':''">Short-loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 2" :class="active1 === 2? 'text-primary border-primary':''">Shortest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 3" :class="active1 === 3? 'text-primary border-primary':''">Longest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 4" :class="active1 === 4? 'text-primary border-primary':''">Most Frequent Trace</li>
</ul>
<div>
<TabView ref="tabview2" v-model:activeIndex="active1">
<TabPanel header="Self-loop">
<p v-if="insights.self_loops.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li v-for="(value, key) in insights.self_loops" :key="key">
<span>{{ value }}</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Short-loop">
<p v-if="insights.short_loops.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.short_loops" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">sync_alt</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Shortest Trace">
<p v-if="insights.shortest_traces.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.shortest_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Longest Trace">
<p v-if="insights.longest_traces.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.longest_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Most Frequent Trace">
<li v-if="insights.most_freq_traces.length === 0">No data</li>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.most_freq_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
</TabView>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import getNumberLabel from '@/module/numberLabel.js';
import getTimeLabel from '@/module/timeLabel.js';
import getMoment from 'moment';
export default {
props:{
sidebarState: {
type: Boolean,
require: false,
},
stats: {
type: Object,
required: false,
},
insights: {
type: Object,
required: false,
}
},
data() {
return {
tab: 'summary',
valueCases: 0,
valueTraces: 0,
valueTaskInstances: 0,
valueTasks: 0,
active1: 0,
}
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* @param {number} time use timeLabel.js
*/
timeLabel(time){
return getTimeLabel(time);
},
/**
* @param {number} time use moment
*/
moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm');
},
/**
* @param {number} num use numberLabel.js
*/
numberLabel(num){
return getNumberLabel(num);
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if(val * 100 === 100) return `${val * 100}%`;
return `${(val * 100).toFixed(1)}%`;
},
/**
* Behavior when show
*/
show(){
this.valueCases = this.stats.cases.ratio * 100;
this.valueTraces= this.stats.traces.ratio * 100;
this.valueTaskInstances = this.stats.task_instances.ratio * 100;
this.valueTasks = this.stats.tasks.ratio * 100;
},
/**
* Behavior when hidden
*/
hide(){
this.valueCases = 0;
this.valueTraces= 0;
this.valueTaskInstances = 0;
this.valueTasks = 0;
},
},
}
</script>
<style scoped>
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
:deep(.p-tabview-nav-container) {
@apply hidden
}
:deep(.p-tabview-panels) {
@apply !bg-neutral-100 p-2 rounded
}
:deep(.p-tabview-panel) {
@apply animate-fadein
}
</style>

View File

@@ -1,207 +0,0 @@
<template>
<Sidebar :visible="sidebarTraces" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" class="!w-11/12" @show="show()">
<template #header>
<p class="h1">Traces</p>
</template>
<div class="pt-4 h-full flex items-center justify-start">
<!-- Trace List -->
<section class="w-80 h-full pr-4 border-r border-neutral-300">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
<span class="material-symbols-outlined text-sm align-[-10%] mr-2">info</span>Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<table class="border-separate border-spacing-x-2 text-sm">
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id)">
<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">{{ trace.ratio }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Trace item Table -->
<section class="pl-4 h-full w-[calc(100%_-_320px)]">
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-52 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_264px)]">
<DataTable :value="cases" showGridlines tableClass="text-sm">
<Column field="id" header="Case ID" sortable></Column>
<Column field="started_at" header="Start time" sortable></Column>
<Column field="completed_at" header="End time" sortable></Column>
</DataTable>
</div>
</section>
</div>
</Sidebar>
</template>
<script>
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
props: {
sidebarTraces: Boolean,
traces: Array,
traceTaskSeq: Array,
cases: Array
},
data() {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: 1,
}
},
computed: {
traceTotal: function() {
return this.traces.length;
},
traceList: function() {
// let list = [];
// this.traces.forEach((trace, index) => {
// let data = {
// id: trace.id,
// value: Number((trace.ratio * 100).toFixed(1)),
// count: trace.count,
// ratio: this.getPercentLabel(trace.ratio),
// };
// list.push(data);
// });
// return list;
return this.traces.map(trace => {
return {
id: trace.id,
value: Number((trace.ratio * 100).toFixed(1)),
count: trace.count,
ratio: this.getPercentLabel(trace.ratio),
};
}).sort((x, y) => x.id - y.id);
},
},
methods: {
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? `${val * 100}%` : `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id
*/
async switchCaseData(id) {
this.showTraceId = id;
this.$emit('switch-Trace-Id', this.showTraceId);
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.traceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.traceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
let graphId = this.$refs.cyTrace;
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
show() {
this.setNodesData();
this.setEdgesData();
this.createCy();
}
},
created(){
},
mounted() {
}
}
</script>
<style scoped>
/* 進度條顏色 */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
/* Table set */
:deep(.p-datatable-thead th) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0
}
</style>

View File

@@ -1,163 +0,0 @@
<template>
<Sidebar :visible="sidebarView" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" >
<template #header>
<p class="h1">Visualization Setting</p>
</template>
<div>
<!-- View -->
<div class="my-4 border-b border-neutral-200">
<p class="h2">View</p>
<ul class="space-y-3 mb-4">
<!-- 選擇 bpmn / processmap button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="switchMapType('processMap')">
Process map
</span>
<span class="btn-toggle-item" :class="mapType === 'bpmn'?'btn-toggle-show':''" @click="switchMapType('bpmn')">
BPMN Model
</span>
</li>
<!-- 選擇繪畫樣式 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')">
Curved
</span>
<span class="btn-toggle-item" :class="curveStyle === 'taxi'?'btn-toggle-show':''" @click="switchCurveStyles('taxi')">
Elbow
</span>
</li>
<!-- 直向 TB | 橫向 LR -->
<li class="btn-toggle-content">
<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')">
Vertical
</span>
</li>
</ul>
</div>
<!-- Data Layer -->
<div>
<p class="h2">Data Layer</p>
<ul class="space-y-2">
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'freq')">
<div class="flex items-center w-1/2">
<input type="radio" id="freq" value="freq" name="dataLayer" class="peer hidden" checked/>
<label for="freq" class="inline-block h-4 w-4 m-2 cursor-pointer rounded-full ring-2 ring-neutral-300 shadow-sm peer-checked:ring-2 peer-checked:ring-primary peer-checked:ring-offset-2 peer-checked:bg-primary">
</label>
<span class="inline-block ml-2">Frequency</span>
</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>
</div>
</li>
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'duration')">
<div class="flex items-center w-1/2">
<input type="radio" id="duration" value="duration" name="dataLayer" class="peer hidden" />
<label for="duration" class="inline-block h-4 w-4 m-2 cursor-pointer rounded-full ring-2 ring-neutral-300 shadow-sm peer-checked:ring-2 peer-checked:ring-primary peer-checked:ring-offset-2 peer-checked:bg-primary"></label>
<span class="inline-block ml-2">Duration</span>
</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>
</div>
</li>
</ul>
</div>
</div>
</Sidebar>
</template>
<script>
export default {
props: {
sidebarView: {
type: Boolean,
require: true,
},
},
data() {
return {
selectFrequency: [
{ 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, },
],
selectDuration:[
{ 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, },
],
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
dataLayerType: 'freq', // freq | duration
dataLayerOption: 'total',
selectedFreq: '',
selectedDuration: '',
rank: 'LR', // 直向 TB | 橫向 LR
}
},
methods: {
/**
* switch map type
* @param {string} type processMap | bpmn
*/
switchMapType(type) {
this.mapType = type;
this.$emit('switch-map-type', this.mapType);
},
/**
* switch curve style
* @param {string} style 直角 unbundled-bezier | taxi
*/
switchCurveStyles(style) {
this.curveStyle = style;
this.$emit('switch-curve-styles', this.curveStyle);
},
/**
* switch rank
* @param {string} rank 直向 TB | 橫向 LR
*/
switchRank(rank) {
this.rank = rank;
this.$emit('switch-rank', this.rank);
},
/**
* switch Data Layoer Type or Option.
* @param {string} e
* @param {string} type freq | duration
*/
switchDataLayerType(e, type){
let value = e.target.type === 'select-one'? e.target.value: 'total';
switch (type) {
case 'freq':
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedFreq = value;
break;
case 'duration':
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedDuration = value;
break;
};
this.$emit('switch-data-layer-type', this.dataLayerType, this.dataLayerOption);
},
}
}
</script>

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