Compare commits

...

980 Commits

Author SHA1 Message Date
imacat f828bd0423 Remove Cypress and update scripts to use Playwright for E2E testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:43:38 +08:00
imacat 6e7d010c54 Add Playwright E2E tests replacing Cypress with MSW integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:43:18 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat d4429571d5 Revert "Use structuredClone instead of JSON.parse(JSON.stringify()) (S7784)"
This reverts commit 2b0dadedd4.
2026-03-10 00:50:31 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat e52a53615f Remove unnecessary async from closeModal()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:41:46 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat ca75a06612 Update .gitignore 2026-03-09 13:19:16 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 7309c97502 Remove redundant self-assignment watcher in navbar state
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 14:14:11 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 52a36e3a7c Resolve remaining lint violations and stabilize ESLint config
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:24:45 +08:00
imacat 847904c49b Apply repository-wide ESLint auto-fix formatting pass
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 12:11:57 +08:00
imacat 7c48faaa3d Translate Cypress support comments and add command JSDoc
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:57:58 +08:00
imacat 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
imacat 28c861ab0e Align Cypress base URL and redirect test port
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:52:34 +08:00
imacat 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
imacat 1ad94358e4 Remove unsupported ignore-path option from lint script
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 11:47:28 +08:00
imacat d7caf08d1f Fix design file metadata mapping in files store
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 10:47:49 +08:00
imacat 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
imacat e275e79a63 Sanitize Cytoscape tooltip labels to prevent XSS
Co-Authored-By: Codex <codex@openai.com>
2026-03-08 10:41:48 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat ddab7b3fe9 Add missing path=/ to setCookieWithoutExpiration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:59:40 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat d16cc46604 Delete yarn.lock 2026-03-06 20:09:02 +08:00
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
imacat 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
302 changed files with 46111 additions and 11333 deletions
+15
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 = ""
-4
View File
@@ -1,4 +0,0 @@
/*
For demo env file
*/
VUE_APP_API_URL = "https://REDACTED-HOST/api/"
-20
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",
},
};
+9 -5
View File
@@ -15,10 +15,9 @@ coverage
*.local *.local
/dist /dist
# Cypress # Playwright
cypress.env.json /test-results/
/cypress/videos/ /playwright-report/
/cypress/screenshots/
# Editor directories and files # Editor directories and files
vscode vscode
@@ -32,11 +31,16 @@ vscode
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.claude
# local env files # local env files
.env.demo
.env.local .env.local
.env.*.local .env.*.local
# TypeDoc generated documentation
/docs
.scannerwork .scannerwork
sonar-project.properties sonar-project.properties
excludes
+145 -28
View File
@@ -1,57 +1,174 @@
# 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), Cypress 15 (E2E) |
| Linting | ESLint, Prettier |
## Prerequisites
- [Node.js][nodejs] (v18 or later)
- npm
## Getting Started
### Install dependencies
```sh ```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 ```sh
npm run dev npm run dev
``` ```
### Compile and Minify for Production The app will be available at `http://localhost:58249`.
## Testing
### Run unit and component tests
```sh ```sh
npm run build npx vitest run
``` ```
### Run Unit Tests with [Vitest](https://vitest.dev/) ### Run E2E tests
```sh Build first, then run [Cypress][cypress] against the preview
npm run test:unit server:
```
### 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 ```sh
npm run build npm run build
npm run test:e2e npm run test:e2e
``` ```
### Lint with [ESLint](https://eslint.org/) For interactive E2E development with the Vite dev server:
```sh ```sh
npm run lint npm run test:e2e:dev
``` ```
## 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/
[cypress]: https://www.cypress.io/
[typedoc]: https://typedoc.org/
[claude-code]: https://claude.ai/claude-code
-8
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}",
},
});
-8
View File
@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["./**/*", "../support/**/*"]
}
-44
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();
});
});
@@ -1,4 +0,0 @@
{
"username": " test ",
"password": " test "
}
-25
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) => { ... })
-24
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')
-28
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
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",
},
},
];
+8 -1
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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -8,6 +15,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+8307 -7713
View File
File diff suppressed because it is too large Load Diff
+58 -39
View File
@@ -1,57 +1,76 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.2.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"build:e2e": "VITE_MSW=true vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"test:unit": "vitest --environment jsdom --root src/", "test:unit": "vitest run",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'", "test:e2e": "playwright test --config tests/e2e/playwright.config.ts",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'", "test:e2e:ui": "playwright test --config tests/e2e/playwright.config.ts --ui",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix",
"docs": "typedoc"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^10.4.13", "@primevue/themes": "^4.5.4",
"axios": "^1.2.2", "@tailwindcss/postcss": "^4.2.1",
"cytoscape": "^3.23.0", "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-dagre": "^2.5.0",
"cytoscape-popper": "^2.0.0", "cytoscape-fcose": "^2.2.0",
"javascript-color-gradient": "^2.4.4", "cytoscape-popper": "^4.0.1",
"mitt": "^3.0.0", "decimal.js": "^10.6.0",
"moment": "^2.29.4", "i18next": "^25.8.14",
"pinia": "^2.0.28", "i18next-browser-languagedetector": "^8.2.1",
"postcss": "^8.4.20", "javascript-color-gradient": "^2.5.0",
"primeicons": "^6.0.1", "lodash-es": "^4.17.23",
"primevue": "^3.23.0", "mitt": "^3.0.1",
"tailwindcss": "^3.2.4", "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", "tippy.js": "^6.3.7",
"vue": "^3.2.45", "vue": "^3.5.29",
"vue-axios": "^3.5.2", "vue-chartjs": "^5.3.3",
"vue-router": "^4.1.6", "vue-router": "^5.0.3",
"vue-toast-notification": "^3.0.4", "vue-sweetalert2": "^5.0.11",
"vue-toast-notification": "^3.1.3",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.4", "@eslint/js": "^10.0.1",
"@vitejs/plugin-vue": "^4.0.0", "@playwright/test": "^1.58.2",
"@vue/eslint-config-prettier": "^7.0.0", "@types/cytoscape": "^3.21.9",
"@vue/test-utils": "^2.2.6", "@types/cytoscape-dagre": "^2.3.4",
"autoprefixer": "^10.4.13", "@types/cytoscape-popper": "^2.0.4",
"cypress": "^12.0.2", "@types/node": "^25.3.5",
"eslint": "^8.22.0", "@vitejs/plugin-vue": "^6.0.4",
"eslint-plugin-cypress": "^2.12.1", "@vue/eslint-config-prettier": "^10.2.0",
"eslint-plugin-vue": "^9.3.0", "@vue/test-utils": "^2.4.6",
"html-webpack-plugin": "^5.5.0", "chartjs-plugin-dragdata": "^2.3.1",
"jsdom": "^20.0.3", "eslint": "^10.0.2",
"postcss": "^8.4.20", "eslint-plugin-vue": "^10.8.0",
"prettier": "^2.7.1", "jsdom": "^28.1.0",
"sass": "^1.57.1", "msw": "^2.12.14",
"start-server-and-test": "^1.15.2", "postcss": "^8.5.8",
"tailwindcss": "^3.2.4", "prettier": "^3.8.1",
"vite": "^4.0.0", "sass": "^1.97.3",
"vitest": "^0.25.6" "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"
} }
} }
+11 -2
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 = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {},
}, },
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+349
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,
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+10 -3
View File
@@ -2,8 +2,15 @@
<RouterView /> <RouterView />
</template> </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 { RouterView } from "vue-router";
import { RouterLink, RouterView } from "vue-router";
</script> </script>
+53
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
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;
+20 -1
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 { @layer base {
html { html {
font-family: 'Roboto', sans-serif, system-ui; font-family: 'Roboto', sans-serif, system-ui;
@@ -18,3 +27,13 @@
.h-screen-main { .h-screen-main {
height: calc(100vh - 104px); height: calc(100vh - 104px);
} }
/* button */
.disable-hover {
@apply pointer-events-none
}
/* Map i panel ; overwrite primevue style */
.p-sidebar .p-sidebar-header {
padding: 8px;
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

+6
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
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

+7
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
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

+8
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
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

+8
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
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

+36 -58
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 */ /* loading */
.loader { .loader {
width: 64px; width: 64px;
@@ -30,62 +38,29 @@
transform: rotate(360deg) transform: rotate(360deg)
} }
} }
/* loaderBar */
/* <span class="loaderBar"></span> */
.loaderBar {
width: 80%;
height: 16px;
display: inline-block;
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 */ @keyframes barStripe {
/* <div class="toggle"> 0% {
<input type="checkbox"/> background-position: 0 0;
<label></label> }
</div> */ 100% {
.toggle { background-position: 1em 0;
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;
display: inline-block;
border-radius: 46px;
transition: 0.2s ease-in;
}
.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;
} }
/* components */ /* components */
@@ -100,11 +75,11 @@
} }
.scrollbar::-webkit-scrollbar-thumb { .scrollbar::-webkit-scrollbar-thumb {
@apply bg-primary rounded-full @apply bg-neutral-300 rounded-full
} }
.scrollbar::-webkit-scrollbar-thumb:hover { .scrollbar::-webkit-scrollbar-thumb:hover {
@apply bg-primary @apply bg-neutral-400
} }
} }
@@ -119,7 +94,10 @@
@apply px-4 py-2.5 @apply px-4 py-2.5
} }
.btn-c-primary { .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 { .btn-disable {
@apply border border-neutral-200 bg-neutral-50 text-neutral-200 @apply border border-neutral-200 bg-neutral-50 text-neutral-200
+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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

+3
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
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

+4
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
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

+11 -1
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 */ /* Navbar */
nav ul>li { 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 */ /* Header */
+9
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 './tailwind.css';
@import './base.css'; @import './base.css';
@import './components.css'; @import './components.css';
@import './layout.css'; @import './layout.css';
@import './vendors.css'; @import './vendors.css';
@import './zindex.css';
+23
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
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

+82 -4
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'); @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; @import "tailwindcss" layer(tailwind-base);
@tailwind components;
@tailwind utilities; @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); }
}
+81 -3
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 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'); @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, 'GRAD' 0,
'opsz' 40 '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 */ /* Primevue */
/* sidebar */
.p-sidebar-left { .p-sidebar-left {
@apply ml-14 @apply ml-14
} }
.p-sidebar-mask { .p-sidebar-mask {
height: calc(100vh - 104px) !important; height: calc(100vh - 104px) !important;
top: 104px !important; top: 104px !important;
z-index: 20;
} }
.p-sidebar { .p-sidebar {
@apply !shadow-[1px_0px_4px_rgba(0,0,0,0.25)] @apply !shadow-[1px_0px_4px_rgba(0,0,0,0.25)]
} }
.p-sidebar-header { .p-sidebar-header {
@apply bg-neutral-200 border-b border-neutral-300 !py-2 !justify-between @apply bg-neutral-200 border-b border-neutral-300 !py-2 !justify-between
}; }
.p-sidebar-right .p-sidebar-header { .p-sidebar-right .p-sidebar-header {
@apply flex-row-reverse !justify-end text-neutral-500 @apply flex-row-reverse !justify-end text-neutral-500
} }
.p-sidebar-right .p-sidebar { .p-sidebar-right .p-sidebar {
@apply !shadow-[-1px_0px_4px_rgba(0,0,0,0.25)] @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
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*/
}
+188
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>
+58
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>
+45
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>
+40
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>
+117
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>
+417
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>
+47
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>
+441
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>
@@ -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
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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
@@ -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>
@@ -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>
+273
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>
+245
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>
-513
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>
-266
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>
-207
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>
-163
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