Compare commits

3 Commits
Author SHA1 Message Date
imacatandClaude Opus 4.6 41d0013bb4 Update README and run-tests for Playwright migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:51:19 +08:00
imacatandClaude Opus 4.6 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
imacatandClaude Opus 4.6 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
48 changed files with 4201 additions and 6540 deletions
+152 -116
View File
@@ -27,13 +27,14 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.5.4", "primevue": "^4.5.4",
"sweetalert2": "^11.4.4",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"vue": "^3.5.29", "vue": "^3.5.29",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-router": "^5.0.3", "vue-router": "^5.0.3",
"vue-sweetalert2": "^5.0.11",
"vue-toast-notification": "^3.1.3", "vue-toast-notification": "^3.1.3",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
@@ -47,8 +48,7 @@
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"ajv": "^8.18.0", "chartjs-plugin-dragdata": "^2.3.1",
"ajv-formats": "^3.0.1",
"eslint": "^10.0.2", "eslint": "^10.0.2",
"eslint-plugin-vue": "^10.8.0", "eslint-plugin-vue": "^10.8.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
@@ -2964,40 +2964,22 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "8.18.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.1",
"fast-uri": "^3.0.1", "fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^0.4.1",
"require-from-string": "^2.0.2" "uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3199,6 +3181,44 @@
"chart.js": ">=3.0.0" "chart.js": ">=3.0.0"
} }
}, },
"node_modules/chartjs-plugin-dragdata": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/chartjs-plugin-dragdata/-/chartjs-plugin-dragdata-2.3.1.tgz",
"integrity": "sha512-CFD1e2d+gyH9EXb92qWu4Zb2zVoY4OtrbJYLMoGWOInE7ftoOD//4B0/k9IvKzQbdVU3JsPqUQI9KHcMpqQVfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0"
},
"peerDependencies": {
"chart.js": "^3.9.1 || ^4.0.1"
}
},
"node_modules/chartjs-plugin-dragdata/node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/chartjs-plugin-dragdata/node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
@@ -3540,6 +3560,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -3970,23 +3996,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": { "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
@@ -4000,13 +4009,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/espree": { "node_modules/espree": {
"version": "11.1.1", "version": "11.1.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz",
@@ -4134,23 +4136,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4786,9 +4771,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "1.0.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -5645,6 +5630,31 @@
} }
} }
}, },
"node_modules/pinia-plugin-persistedstate": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
"integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
"license": "MIT",
"dependencies": {
"defu": "^6.1.4"
},
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"@pinia/nuxt": ">=0.10.0",
"pinia": ">=3.0.0"
},
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@pinia/nuxt": {
"optional": true
},
"pinia": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": { "node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.9", "version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
@@ -6171,7 +6181,6 @@
"version": "11.4.4", "version": "11.4.4",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.4.tgz", "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.4.tgz",
"integrity": "sha512-9yYWQuRT1v9JNI/paPTSYV+68MHwe9C+HQ/I2jtfaFzoJgYRftWXOs4JqmDSjT7m2m4r8ebMMn8LcxD1Wq9B/w==", "integrity": "sha512-9yYWQuRT1v9JNI/paPTSYV+68MHwe9C+HQ/I2jtfaFzoJgYRftWXOs4JqmDSjT7m2m4r8ebMMn8LcxD1Wq9B/w==",
"license": "MIT",
"funding": { "funding": {
"url": "https://sweetalert2.github.io/#donations" "url": "https://sweetalert2.github.io/#donations"
} }
@@ -6875,6 +6884,17 @@
} }
} }
}, },
"node_modules/vue-sweetalert2": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/vue-sweetalert2/-/vue-sweetalert2-5.0.11.tgz",
"integrity": "sha512-agujdlcNeLb4Fj0/V3qgR7bEVBN8IzWFoDBP/vrSgbEGl9pjMmxnv32V3lxB0SZtQh6tL6R/UZ+C3q8tzsJbNg==",
"dependencies": {
"sweetalert2": "11.4.4"
},
"peerDependencies": {
"vue": "*"
}
},
"node_modules/vue-toast-notification": { "node_modules/vue-toast-notification": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz",
@@ -8796,24 +8816,15 @@
"dev": true "dev": true
}, },
"ajv": { "ajv": {
"version": "8.18.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"requires": { "requires": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.1",
"fast-uri": "^3.0.1", "fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^0.4.1",
"require-from-string": "^2.0.2" "uri-js": "^4.2.2"
}
},
"ajv-formats": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"dev": true,
"requires": {
"ajv": "^8.0.0"
} }
}, },
"ansi-regex": { "ansi-regex": {
@@ -8961,6 +8972,34 @@
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
"requires": {} "requires": {}
}, },
"chartjs-plugin-dragdata": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/chartjs-plugin-dragdata/-/chartjs-plugin-dragdata-2.3.1.tgz",
"integrity": "sha512-CFD1e2d+gyH9EXb92qWu4Zb2zVoY4OtrbJYLMoGWOInE7ftoOD//4B0/k9IvKzQbdVU3JsPqUQI9KHcMpqQVfg==",
"dev": true,
"requires": {
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0"
},
"dependencies": {
"d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dev": true,
"requires": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
}
},
"d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true
}
}
},
"chokidar": { "chokidar": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
@@ -9215,6 +9254,11 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true "dev": true
}, },
"defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -9425,29 +9469,11 @@
"optionator": "^0.9.3" "optionator": "^0.9.3"
}, },
"dependencies": { "dependencies": {
"ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"eslint-visitor-keys": { "eslint-visitor-keys": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true "dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
} }
} }
}, },
@@ -9601,12 +9627,6 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true "dev": true
}, },
"fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true
},
"file-entry-cache": { "file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -10015,9 +10035,9 @@
"dev": true "dev": true
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true "dev": true
}, },
"json-stable-stringify-without-jsonify": { "json-stable-stringify-without-jsonify": {
@@ -10529,6 +10549,14 @@
} }
} }
}, },
"pinia-plugin-persistedstate": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
"integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
"requires": {
"defu": "^6.1.4"
}
},
"pkg-types": { "pkg-types": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
@@ -11251,6 +11279,14 @@
"yaml": "^2.8.2" "yaml": "^2.8.2"
} }
}, },
"vue-sweetalert2": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/vue-sweetalert2/-/vue-sweetalert2-5.0.11.tgz",
"integrity": "sha512-agujdlcNeLb4Fj0/V3qgR7bEVBN8IzWFoDBP/vrSgbEGl9pjMmxnv32V3lxB0SZtQh6tL6R/UZ+C3q8tzsJbNg==",
"requires": {
"sweetalert2": "11.4.4"
}
},
"vue-toast-notification": { "vue-toast-notification": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz",
+3 -3
View File
@@ -36,13 +36,14 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.5.4", "primevue": "^4.5.4",
"sweetalert2": "^11.4.4",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"vue": "^3.5.29", "vue": "^3.5.29",
"vue-chartjs": "^5.3.3", "vue-chartjs": "^5.3.3",
"vue-router": "^5.0.3", "vue-router": "^5.0.3",
"vue-sweetalert2": "^5.0.11",
"vue-toast-notification": "^3.1.3", "vue-toast-notification": "^3.1.3",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
@@ -56,8 +57,7 @@
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"ajv": "^8.18.0", "chartjs-plugin-dragdata": "^2.3.1",
"ajv-formats": "^3.0.1",
"eslint": "^10.0.2", "eslint": "^10.0.2",
"eslint-plugin-vue": "^10.8.0", "eslint-plugin-vue": "^10.8.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
@@ -358,8 +358,8 @@
<MoreModal <MoreModal
:listModal="issuesModal" :listModal="issuesModal"
@closeModal="issuesModal = $event" @closeModal="issuesModal = $event"
:listTraces="allIssueTraces" :listTraces="issueTraces"
:taskSeq="allTaskSeq" :taskSeq="taskSeq"
:cases="cases" :cases="cases"
:listNo="issuesNo" :listNo="issuesNo"
:traceId="traceId" :traceId="traceId"
@@ -369,8 +369,8 @@
<MoreModal <MoreModal
:listModal="loopModal" :listModal="loopModal"
@closeModal="loopModal = $event" @closeModal="loopModal = $event"
:listTraces="allLoopTraces" :listTraces="loopTraces"
:taskSeq="allLoopTaskSeq" :taskSeq="loopTaskSeq"
:cases="loopCases" :cases="loopCases"
:listNo="loopNo" :listNo="loopNo"
:traceId="looptraceId" :traceId="looptraceId"
@@ -413,12 +413,12 @@ import emitter from "@/utils/emitter";
const conformanceStore = useConformanceStore(); const conformanceStore = useConformanceStore();
const { const {
allConformanceTempReportData, conformanceTempReportData,
allIssueTraces, issueTraces,
allTaskSeq, taskSeq,
cases, cases,
allLoopTraces, loopTraces,
allLoopTaskSeq, loopTaskSeq,
loopCases, loopCases,
} = storeToRefs(conformanceStore); } = storeToRefs(conformanceStore);
@@ -525,11 +525,11 @@ const openMore = async (no) => {
// Use async/await to prevent errors caused by asynchronous data not being available yet // Use async/await to prevent errors caused by asynchronous data not being available yet
issuesNo.value = no; issuesNo.value = no;
await conformanceStore.getConformanceIssue(no); await conformanceStore.getConformanceIssue(no);
if (allIssueTraces.value.length === 0) return; if (issueTraces.value.length === 0) return;
traceId.value = allIssueTraces.value[0].id; traceId.value = issueTraces.value[0].id;
firstCases.value = await conformanceStore.getConformanceTraceDetail( firstCases.value = await conformanceStore.getConformanceTraceDetail(
no, no,
allIssueTraces.value[0].id, issueTraces.value[0].id,
0, 0,
); );
issuesModal.value = true; issuesModal.value = true;
@@ -543,11 +543,11 @@ const openLoopMore = async (no) => {
// Use async/await to prevent errors caused by asynchronous data not being available yet // Use async/await to prevent errors caused by asynchronous data not being available yet
loopNo.value = no; loopNo.value = no;
await conformanceStore.getConformanceLoop(no); await conformanceStore.getConformanceLoop(no);
if (allLoopTraces.value.length === 0) return; if (loopTraces.value.length === 0) return;
looptraceId.value = allLoopTraces.value[0].id; looptraceId.value = loopTraces.value[0].id;
loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail( loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail(
no, no,
allLoopTraces.value[0].id, loopTraces.value[0].id,
0, 0,
); );
loopModal.value = true; loopModal.value = true;
@@ -959,7 +959,7 @@ const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
}; };
// watch // watch
watch(allConformanceTempReportData, (newValue) => { watch(conformanceTempReportData, (newValue) => {
if (newValue?.rule && newValue.rule.min !== null) { if (newValue?.rule && newValue.rule.min !== null) {
selectDurationTime.value = { selectDurationTime.value = {
min: newValue.rule.min, min: newValue.rule.min,
@@ -180,7 +180,7 @@ const {
selectedProcessScope, selectedProcessScope,
selectedActSeqMore, selectedActSeqMore,
selectedActSeqFromTo, selectedActSeqFromTo,
allConformanceTask, conformanceAllTasks,
cfmPtEteWhole, cfmPtEteWhole,
cfmPtEteStart: cfmPtEteStartStore, cfmPtEteStart: cfmPtEteStartStore,
cfmPtEteEnd: cfmPtEteEndStore, cfmPtEteEnd: cfmPtEteEndStore,
@@ -204,7 +204,7 @@ const {
conformanceRuleData, conformanceRuleData,
conformanceLogCreateCheckId, conformanceLogCreateCheckId,
conformanceFilterCreateCheckId, conformanceFilterCreateCheckId,
allConformanceTempReportData, conformanceTempReportData,
} = storeToRefs(conformanceStore); } = storeToRefs(conformanceStore);
const { activityRadioData } = storeToRefs(conformanceInputStore); const { activityRadioData } = storeToRefs(conformanceInputStore);
@@ -391,7 +391,7 @@ const isApplyBtnDisabled = computed(() => {
// When opening a rule file, display the saved options, rules, and time // When opening a rule file, display the saved options, rules, and time
let reportWatchTimerId = null; let reportWatchTimerId = null;
watch(allConformanceTempReportData, (newValue) => { watch(conformanceTempReportData, (newValue) => {
if (reportWatchTimerId) clearTimeout(reportWatchTimerId); if (reportWatchTimerId) clearTimeout(reportWatchTimerId);
reportWatchTimerId = setTimeout(() => { reportWatchTimerId = setTimeout(() => {
if (newValue !== null) { if (newValue !== null) {
@@ -376,8 +376,8 @@ const {
selectedActSeqMore, selectedActSeqMore,
selectedActSeqFromTo, selectedActSeqFromTo,
conformanceTask, conformanceTask,
allCfmSeqStart, cfmSeqStart,
allCfmSeqEnd, cfmSeqEnd,
cfmPtEteStart, cfmPtEteStart,
cfmPtEteEnd, cfmPtEteEnd,
cfmPtEteSE, cfmPtEteSE,
@@ -435,14 +435,14 @@ const taskEnd = ref(null);
const cfmSeqStartData = computed(() => { const cfmSeqStartData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null; const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
return isEndSelected.value return isEndSelected.value
? setSeqStartAndEndData(allCfmSeqEnd.value, "sources", selectedTask) ? setSeqStartAndEndData(cfmSeqEnd.value, "sources", selectedTask)
: allCfmSeqStart.value.map((i) => i.label); : cfmSeqStart.value.map((i) => i.label);
}); });
const cfmSeqEndData = computed(() => { const cfmSeqEndData = computed(() => {
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null; const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
return isStartSelected.value return isStartSelected.value
? setSeqStartAndEndData(allCfmSeqStart.value, "sinks", selectedTask) ? setSeqStartAndEndData(cfmSeqStart.value, "sinks", selectedTask)
: allCfmSeqEnd.value.map((i) => i.label); : cfmSeqEnd.value.map((i) => i.label);
}); });
// Processing time // Processing time
const cfmPtEteStartData = computed(() => { const cfmPtEteStartData = computed(() => {
@@ -584,7 +584,7 @@ watch(
/** /**
* Sets the start and end radio data. * Sets the start and end radio data.
* @param {object} data - Activities list data from the backend (allCfmSeqStart, allCfmSeqEnd, cfmPtEteSE, etc.). * @param {object} data - Activities list data from the backend (cfmSeqStart, cfmSeqEnd, cfmPtEteSE, etc.).
* @param {string} category - 'start' or 'end'. * @param {string} category - 'start' or 'end'.
* @returns {array} * @returns {array}
*/ */
@@ -615,7 +615,7 @@ function setStartAndEndData(data, category, taskVal) {
} }
/** /**
* Resets the activity sequence linked start and end radio data. * Resets the activity sequence linked start and end radio data.
* @param {object} data - Activities list data from the backend (allCfmSeqStart or allCfmSeqEnd). * @param {object} data - Activities list data from the backend (cfmSeqStart or cfmSeqEnd).
* @param {string} category - 'sources' or 'sinks'. * @param {string} category - 'sources' or 'sinks'.
* @param {string} task - The selected activity task. * @param {string} task - The selected activity task.
* @returns {array} * @returns {array}
@@ -250,10 +250,10 @@ const {
selectedProcessScope, selectedProcessScope,
selectedActSeqMore, selectedActSeqMore,
selectedActSeqFromTo, selectedActSeqFromTo,
allConformanceTask, conformanceAllTasks,
conformanceTask, conformanceTask,
allCfmSeqStart, cfmSeqStart,
allCfmSeqEnd, cfmSeqEnd,
cfmPtEteStart, cfmPtEteStart,
cfmPtEteEnd, cfmPtEteEnd,
cfmPtEteSE, cfmPtEteSE,
@@ -471,7 +471,7 @@ emitter.on("actRadioData", (data) => {
// Activity duration // Activity duration
case "cfmDur": case "cfmDur":
state.timeDuration = getDurationTime( state.timeDuration = getDurationTime(
allConformanceTask.value, conformanceAllTasks.value,
"act", "act",
task, task,
); );
@@ -83,7 +83,7 @@ import getMoment from "moment";
const props = defineProps(["selectValue"]); const props = defineProps(["selectValue"]);
const allMapDataStore = useAllMapDataStore(); const allMapDataStore = useAllMapDataStore();
const { allFilterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore); const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
const selectRange = ref(1000); // Number of divisions for the selection range const selectRange = ref(1000); // Number of divisions for the selection range
const selectArea = ref(null); const selectArea = ref(null);
@@ -120,8 +120,8 @@ watch(timeFrameStartEnd, (newValue) => {
// Compute slider data; time format: millisecond timestamps // Compute slider data; time format: millisecond timestamps
const sliderData = computed(() => { const sliderData = computed(() => {
const xAxisMin = new Date(allFilterTimeframe.value.x_axis.min).getTime(); const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
const xAxisMax = new Date(allFilterTimeframe.value.x_axis.max).getTime(); const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
const range = xAxisMax - xAxisMin; const range = xAxisMax - xAxisMin;
const step = range / selectRange.value; const step = range / selectRange.value;
const data = []; const data = [];
@@ -135,9 +135,9 @@ const sliderData = computed(() => {
// Add the minimum and maximum values // Add the minimum and maximum values
const timeFrameData = computed(() => { const timeFrameData = computed(() => {
if (!allFilterTimeframe.value?.data || allFilterTimeframe.value.data.length < 10) if (!filterTimeframe.value?.data || filterTimeframe.value.data.length < 10)
return []; return [];
const data = allFilterTimeframe.value.data.map((i) => ({ x: i.x, y: i.y })); const data = filterTimeframe.value.data.map((i) => ({ x: i.x, y: i.y }));
// See ./public/timeFrameSlope for the y-axis slope calculation diagram // See ./public/timeFrameSlope for the y-axis slope calculation diagram
// x values are 0 ~ 11, // x values are 0 ~ 11,
// Name three coordinates (ax, ay), (bx, by), (cx, cy) as (a, b), (c, d), (e, f) // Name three coordinates (ax, ay), (bx, by), (cx, cy) as (a, b), (c, d), (e, f)
@@ -148,18 +148,18 @@ const timeFrameData = computed(() => {
const a = 0; const a = 0;
let b; let b;
const c = 1; const c = 1;
const d = allFilterTimeframe.value.data[0].y; const d = filterTimeframe.value.data[0].y;
const e = 2; const e = 2;
const f = allFilterTimeframe.value.data[1].y; const f = filterTimeframe.value.data[1].y;
b = (e * d - a * d - f * a - f * c) / (e - c - a); b = (e * d - a * d - f * a - f * c) / (e - c - a);
if (b < 0) { if (b < 0) {
b = 0; b = 0;
} }
// Y-axis maximum value // Y-axis maximum value
const ma = 9; const ma = 9;
const mb = allFilterTimeframe.value.data[8].y; const mb = filterTimeframe.value.data[8].y;
const mc = 10; const mc = 10;
const md = allFilterTimeframe.value.data[9].y; const md = filterTimeframe.value.data[9].y;
const me = 11; const me = 11;
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc); let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
if (mf < 0) { if (mf < 0) {
@@ -168,12 +168,12 @@ const timeFrameData = computed(() => {
// Add the minimum value // Add the minimum value
data.unshift({ data.unshift({
x: allFilterTimeframe.value.x_axis.min_base, x: filterTimeframe.value.x_axis.min_base,
y: b, y: b,
}); });
// Add the maximum value // Add the maximum value
data.push({ data.push({
x: allFilterTimeframe.value.x_axis.max_base, x: filterTimeframe.value.x_axis.max_base,
y: mf, y: mf,
}); });
@@ -181,8 +181,8 @@ const timeFrameData = computed(() => {
}); });
const labelsData = computed(() => { const labelsData = computed(() => {
const min = new Date(allFilterTimeframe.value.x_axis.min_base).getTime(); const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
const max = new Date(allFilterTimeframe.value.x_axis.max_base).getTime(); const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
const numPoints = 11; const numPoints = 11;
const step = (max - min) / (numPoints - 1); const step = (max - min) / (numPoints - 1);
const data = []; const data = [];
@@ -195,8 +195,8 @@ const labelsData = computed(() => {
watch(selectTimeFrame, (newValue, oldValue) => { watch(selectTimeFrame, (newValue, oldValue) => {
if (newValue.length === 0) { if (newValue.length === 0) {
startTime.value = new Date(allFilterTimeframe.value.x_axis.min); startTime.value = new Date(filterTimeframe.value.x_axis.min);
endTime.value = new Date(allFilterTimeframe.value.x_axis.max); endTime.value = new Date(filterTimeframe.value.x_axis.max);
selectArea.value = [0, selectRange.value]; selectArea.value = [0, selectRange.value];
resizeMask(chart.value); resizeMask(chart.value);
} }
@@ -247,7 +247,7 @@ function resizeRightMask(chartInstance, to) {
* Creates and renders the Chart.js area chart for timeframe data. * Creates and renders the Chart.js area chart for timeframe data.
*/ */
function createChart() { function createChart() {
const max = allFilterTimeframe.value.y_axis.max * 1.1; const max = filterTimeframe.value.y_axis.max * 1.1;
const minX = timeFrameData.value[0]?.x; const minX = timeFrameData.value[0]?.x;
const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x; const maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
@@ -403,10 +403,10 @@ onMounted(() => {
// Slider // Slider
selectArea.value = [0, selectRange.value]; selectArea.value = [0, selectRange.value];
// Calendar // Calendar
startMinDate.value = new Date(allFilterTimeframe.value.x_axis.min); startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
startMaxDate.value = new Date(allFilterTimeframe.value.x_axis.max); startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
endMinDate.value = new Date(allFilterTimeframe.value.x_axis.min); endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
endMaxDate.value = new Date(allFilterTimeframe.value.x_axis.max); endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
// Set the calendar range to match the timeline range // Set the calendar range to match the timeline range
startTime.value = startMinDate.value; startTime.value = startMinDate.value;
endTime.value = startMaxDate.value; endTime.value = startMaxDate.value;
@@ -146,8 +146,8 @@ const {
infinit404, infinit404,
baseInfiniteStart, baseInfiniteStart,
baseTraces, baseTraces,
allBaseTraceTaskSeq, baseTraceTaskSeq,
allBaseCase, baseCases,
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
const { isLoading } = storeToRefs(loadingStore); const { isLoading } = storeToRefs(loadingStore);
@@ -240,7 +240,7 @@ const caseData = computed(() => {
}); });
const columnData = computed(() => { const columnData = computed(() => {
const data = cloneDeep(allBaseCase.value); const data = cloneDeep(baseCases.value);
let result = [ let result = [
{ field: "id", header: "Case Id" }, { field: "id", header: "Case Id" },
{ field: "started_at", header: "Start time" }, { field: "started_at", header: "Start time" },
@@ -378,7 +378,7 @@ function setNodesData() {
// Clear nodes to prevent accumulation on each render // Clear nodes to prevent accumulation on each render
processMap.value.nodes = []; processMap.value.nodes = [];
// Populate nodes with data returned from the API call // Populate nodes with data returned from the API call
allBaseTraceTaskSeq.value.forEach((node, index) => { baseTraceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({ processMap.value.nodes.push({
data: { data: {
id: index, id: index,
@@ -398,7 +398,7 @@ function setNodesData() {
*/ */
function setEdgesData() { function setEdgesData() {
processMap.value.edges = []; processMap.value.edges = [];
allBaseTraceTaskSeq.value.forEach((edge, index) => { baseTraceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({ processMap.value.edges.push({
data: { data: {
source: `${index}`, source: `${index}`,
@@ -431,7 +431,7 @@ function createCy() {
function handleScroll(event) { function handleScroll(event) {
if ( if (
infinitMaxItems.value || infinitMaxItems.value ||
allBaseCase.value.length < 20 || baseCases.value.length < 20 ||
infiniteFinish.value === false infiniteFinish.value === false
) )
return; return;
@@ -452,7 +452,7 @@ async function fetchData() {
infiniteFinish.value = false; infiniteFinish.value = false;
baseInfiniteStart.value += 20; baseInfiniteStart.value += 20;
await allMapDataStore.getBaseTraceDetail(); await allMapDataStore.getBaseTraceDetail();
infiniteData.value = [...infiniteData.value, ...allBaseCase.value]; infiniteData.value = [...infiniteData.value, ...baseCases.value];
infiniteFinish.value = true; infiniteFinish.value = true;
isLoading.value = false; isLoading.value = false;
} catch (error) { } catch (error) {
@@ -128,7 +128,7 @@ const {
infiniteStart, infiniteStart,
traceId, traceId,
traces, traces,
allTraceTaskSeq, traceTaskSeq,
infiniteFirstCases, infiniteFirstCases,
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
@@ -261,7 +261,7 @@ function setNodesData() {
// Clear nodes to prevent accumulation on each render // Clear nodes to prevent accumulation on each render
processMap.value.nodes = []; processMap.value.nodes = [];
// Populate nodes with data returned from the API call // Populate nodes with data returned from the API call
allTraceTaskSeq.value.forEach((node, index) => { traceTaskSeq.value.forEach((node, index) => {
processMap.value.nodes.push({ processMap.value.nodes.push({
data: { data: {
id: index, id: index,
@@ -281,7 +281,7 @@ function setNodesData() {
*/ */
function setEdgesData() { function setEdgesData() {
processMap.value.edges = []; processMap.value.edges = [];
allTraceTaskSeq.value.forEach((edge, index) => { traceTaskSeq.value.forEach((edge, index) => {
processMap.value.edges.push({ processMap.value.edges.push({
data: { data: {
source: `${index}`, source: `${index}`,
+20 -20
View File
@@ -167,7 +167,7 @@ import getMoment from "moment";
const route = useRoute(); const route = useRoute();
const allMapDataStore = useAllMapDataStore(); const allMapDataStore = useAllMapDataStore();
const { logId, allStats, createFilterId } = storeToRefs(allMapDataStore); const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
const isPanel = ref(false); const isPanel = ref(false);
const statData = ref(null); const statData = ref(null);
@@ -184,37 +184,37 @@ function getPercentLabel(val) {
/** Transforms raw stats into display-ready format with localized numbers and time labels. */ /** Transforms raw stats into display-ready format with localized numbers and time labels. */
function getStatData() { function getStatData() {
if (!allStats.value) return; if (!stats.value) return;
statData.value = { statData.value = {
cases: { cases: {
count: allStats.value.cases.count.toLocaleString("en-US"), count: stats.value.cases.count.toLocaleString("en-US"),
total: allStats.value.cases.total.toLocaleString("en-US"), total: stats.value.cases.total.toLocaleString("en-US"),
ratio: getPercentLabel(allStats.value.cases.ratio), ratio: getPercentLabel(stats.value.cases.ratio),
}, },
traces: { traces: {
count: allStats.value.traces.count.toLocaleString("en-US"), count: stats.value.traces.count.toLocaleString("en-US"),
total: allStats.value.traces.total.toLocaleString("en-US"), total: stats.value.traces.total.toLocaleString("en-US"),
ratio: getPercentLabel(allStats.value.traces.ratio), ratio: getPercentLabel(stats.value.traces.ratio),
}, },
task_instances: { task_instances: {
count: allStats.value.task_instances.count.toLocaleString("en-US"), count: stats.value.task_instances.count.toLocaleString("en-US"),
total: allStats.value.task_instances.total.toLocaleString("en-US"), total: stats.value.task_instances.total.toLocaleString("en-US"),
ratio: getPercentLabel(allStats.value.task_instances.ratio), ratio: getPercentLabel(stats.value.task_instances.ratio),
}, },
tasks: { tasks: {
count: allStats.value.tasks.count.toLocaleString("en-US"), count: stats.value.tasks.count.toLocaleString("en-US"),
total: allStats.value.tasks.total.toLocaleString("en-US"), total: stats.value.tasks.total.toLocaleString("en-US"),
ratio: getPercentLabel(allStats.value.tasks.ratio), ratio: getPercentLabel(stats.value.tasks.ratio),
}, },
started_at: getMoment(allStats.value.started_at).format("YYYY-MM-DD HH:mm"), started_at: getMoment(stats.value.started_at).format("YYYY-MM-DD HH:mm"),
completed_at: getMoment(allStats.value.completed_at).format( completed_at: getMoment(stats.value.completed_at).format(
"YYYY-MM-DD HH:mm", "YYYY-MM-DD HH:mm",
), ),
case_duration: { case_duration: {
min: getTimeLabel(allStats.value.case_duration.min), min: getTimeLabel(stats.value.case_duration.min),
max: getTimeLabel(allStats.value.case_duration.max), max: getTimeLabel(stats.value.case_duration.max),
average: getTimeLabel(allStats.value.case_duration.average), average: getTimeLabel(stats.value.case_duration.average),
median: getTimeLabel(allStats.value.case_duration.median), median: getTimeLabel(stats.value.case_duration.median),
}, },
}; };
} }
+3 -1
View File
@@ -20,6 +20,7 @@ import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre"; import dagre from "cytoscape-dagre";
import popper from "cytoscape-popper"; import popper from "cytoscape-popper";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import VueSweetalert2 from "vue-sweetalert2";
// import CSS // import CSS
import "./assets/main.css"; import "./assets/main.css";
@@ -66,6 +67,7 @@ cytoscape.use(popper((ref) => ref));
app.use(pinia); app.use(pinia);
app.use(router); app.use(router);
app.use(VueSweetalert2);
app.use(ToastPlugin, { app.use(ToastPlugin, {
// use `this.$toast` in Vue.js // use `this.$toast` in Vue.js
position: "bottom", position: "bottom",
@@ -105,7 +107,7 @@ async function enableMocking() {
const { http, HttpResponse } = await import("msw"); const { http, HttpResponse } = await import("msw");
const { worker } = await import("./mocks/browser.js"); const { worker } = await import("./mocks/browser.js");
await worker.start({ onUnhandledRequest: "bypass" }); await worker.start({ onUnhandledRequest: "bypass" });
// Expose on window so Playwright can add per-test overrides // Expose on window so Cypress can add per-test overrides
window.__mswWorker__ = worker; window.__mswWorker__ = worker;
window.__msw__ = { http, HttpResponse }; window.__msw__ = { http, HttpResponse };
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -146,49 +146,49 @@
"timeframe": { "timeframe": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": 347 "y": 347
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": 426 "y": 426
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": 394 "y": 394
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": 375 "y": 375
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": 431 "y": 431
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": 393 "y": 393
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": 284 "y": 284
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": 359 "y": 359
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": 386 "y": 386
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": 327 "y": 327
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": 0, "min": 0,
+182 -182
View File
@@ -3,101 +3,101 @@
"avg_cycle_time": { "avg_cycle_time": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": "PT272H17M" "y": 980220.0
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": "PT277H52M56.13S" "y": 1000376.129032
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": "PT253H19M50.0S" "y": 911990.0
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": "PT289H24M20.0S" "y": 1041860.0
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": "PT273H43M35.62S" "y": 985415.625
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": "PT260H34M39.13S" "y": 938079.130435
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": "PT298H31M20.0S" "y": 1074680.0
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": "PT294H57M28.7S" "y": 1061848.695652
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": "PT269H28M39.23S" "y": 970119.230769
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": "PT294H38M23.08S" "y": 1060703.076923
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": "PT0S", "min": 0.0,
"max": "PT298H31M20.0S" "max": 1074680.0
} }
}, },
"avg_cycle_efficiency": { "avg_cycle_efficiency": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": 0.9527980523449506 "y": 0.9527980523449506
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": 0.9516493513262202 "y": 0.9516493513262202
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": 0.9475330648076836 "y": 0.9475330648076836
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": 0.9537265449333607 "y": 0.9537265449333607
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": 0.9528919667258132 "y": 0.9528919667258132
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": 0.9489804015433904 "y": 0.9489804015433904
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": 0.9538748758272698 "y": 0.9538748758272698
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": 0.9548679615433759 "y": 0.9548679615433759
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": 0.9469965631092006 "y": 0.9469965631092006
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": 0.9505469198562757 "y": 0.9505469198562757
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": 0.0, "min": 0.0,
@@ -107,104 +107,104 @@
"avg_process_time": { "avg_process_time": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": "PT260H17M47.37S" "y": 937067.368421
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": "PT264H56M7.74S" "y": 953767.741935
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": "PT240H29M40.0S" "y": 865780.0
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": "PT276H16M40.0S" "y": 994600.0
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": "PT261H3M15.0S" "y": 939795.0
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": "PT247H29M7.83S" "y": 890947.826087
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": "PT285H5M45.71S" "y": 1026345.714286
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": "PT282H19M23.48S" "y": 1016363.478261
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": "PT256H33M46.15S" "y": 923626.153846
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": "PT280H59M" "y": 1011540.0
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": "PT0S", "min": 0.0,
"max": "PT285H5M45.71S" "max": 1026345.714286
} }
}, },
"avg_process_time_by_task": { "avg_process_time_by_task": {
"data": [ "data": [
{ {
"x": "aT00:00:00Z", "x": "a",
"y": "PT36H25M47.49S" "y": 131147.486631
}, },
{ {
"x": "bT00:00:00Z", "x": "b",
"y": "PT37H57M7.06S" "y": 136627.058824
}, },
{ {
"x": "cT00:00:00Z", "x": "c",
"y": "PT37H1M1.25S" "y": 133261.25
}, },
{ {
"x": "dT00:00:00Z", "x": "d",
"y": "PT36H51M37.1S" "y": 132697.095436
}, },
{ {
"x": "eT00:00:00Z", "x": "e",
"y": "PT34H34M2.89S" "y": 124442.891566
}, },
{ {
"x": "fT00:00:00Z", "x": "f",
"y": "PT35H19M35.18S" "y": 127175.180723
}, },
{ {
"x": "gT00:00:00Z", "x": "g",
"y": "PT35H27M7.83S" "y": 127627.826087
}, },
{ {
"x": "hT00:00:00Z", "x": "h",
"y": "PT35H36M3.68S" "y": 128163.680982
}, },
{ {
"x": "iT00:00:00Z", "x": "i",
"y": "PT34H53M8.76S" "y": 125588.756757
}, },
{ {
"x": "jT00:00:00Z", "x": "j",
"y": "PT28H8M10.91S" "y": 101290.909091
}, },
{ {
"x": "kT00:00:00Z", "x": "k",
"y": "PT39H35M43.75S" "y": 142543.75
}, },
{ {
"x": "lT00:00:00Z", "x": "l",
"y": "PT38H21M10.88S" "y": 138070.879121
} }
], ],
"x_axis": { "x_axis": {
@@ -224,60 +224,60 @@
] ]
}, },
"y_axis": { "y_axis": {
"min": "PT0S", "min": 0.0,
"max": "PT39H35M43.75S" "max": 142543.75
} }
}, },
"avg_waiting_time": { "avg_waiting_time": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": "PT11H59M12.63S" "y": 43152.631579
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": "PT12H56M48.39S" "y": 46608.387097
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": "PT12H50M10.0S" "y": 46210.0
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": "PT13H7M40.0S" "y": 47260.0
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": "PT12H40M20.62S" "y": 45620.625
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": "PT13H5M31.3S" "y": 47131.304348
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": "PT13H25M34.29S" "y": 48334.285714
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": "PT12H38M5.22S" "y": 45485.217391
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": "PT12H54M53.08S" "y": 46493.076923
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": "PT13H39M23.08S" "y": 49163.076923
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": "PT0S", "min": 0.0,
"max": "PT13H39M23.08S" "max": 49163.076923
} }
}, },
"avg_waiting_time_by_edge": { "avg_waiting_time_by_edge": {
@@ -287,322 +287,322 @@
"a", "a",
"a" "a"
], ],
"y": "PT1H47M" "y": 6420.0
}, },
{ {
"x": [ "x": [
"a", "a",
"d" "d"
], ],
"y": "PT2H5M6.35S" "y": 7506.352941
}, },
{ {
"x": [ "x": [
"a", "a",
"f" "f"
], ],
"y": "PT1H39M" "y": 5940.0
}, },
{ {
"x": [ "x": [
"a", "a",
"g" "g"
], ],
"y": "PT1H26M15.0S" "y": 5175.0
}, },
{ {
"x": [ "x": [
"a", "a",
"i" "i"
], ],
"y": "PT1H44M20.0S" "y": 6260.0
}, },
{ {
"x": [ "x": [
"b", "b",
"a" "a"
], ],
"y": "PT1H54M" "y": 6840.0
}, },
{ {
"x": [ "x": [
"b", "b",
"b" "b"
], ],
"y": "PT59M" "y": 3540.0
}, },
{ {
"x": [ "x": [
"b", "b",
"g" "g"
], ],
"y": "PT2H1M13.64S" "y": 7273.636364
}, },
{ {
"x": [ "x": [
"b", "b",
"i" "i"
], ],
"y": "PT1H44M48.0S" "y": 6288.0
}, },
{ {
"x": [ "x": [
"c", "c",
"g" "g"
], ],
"y": "PT3H11M" "y": 11460.0
}, },
{ {
"x": [ "x": [
"c", "c",
"h" "h"
], ],
"y": "PT1H53M41.74S" "y": 6821.73913
}, },
{ {
"x": [ "x": [
"c", "c",
"k" "k"
], ],
"y": "PT3H45M" "y": 13500.0
}, },
{ {
"x": [ "x": [
"d", "d",
"c" "c"
], ],
"y": "PT3H16M" "y": 11760.0
}, },
{ {
"x": [ "x": [
"d", "d",
"e" "e"
], ],
"y": "PT1H59M26.69S" "y": 7166.694915
}, },
{ {
"x": [ "x": [
"d", "d",
"g" "g"
], ],
"y": "PT2H14M40.0S" "y": 8080.0
}, },
{ {
"x": [ "x": [
"d", "d",
"i" "i"
], ],
"y": "PT1H" "y": 3600.0
}, },
{ {
"x": [ "x": [
"e", "e",
"a" "a"
], ],
"y": "PT2H1M" "y": 7260.0
}, },
{ {
"x": [ "x": [
"e", "e",
"d" "d"
], ],
"y": "PT1H53M" "y": 6780.0
}, },
{ {
"x": [ "x": [
"e", "e",
"f" "f"
], ],
"y": "PT2H1M28.47S" "y": 7288.474576
}, },
{ {
"x": [ "x": [
"e", "e",
"g" "g"
], ],
"y": "PT3H54M" "y": 14040.0
}, },
{ {
"x": [ "x": [
"e", "e",
"k" "k"
], ],
"y": "PT3H47M" "y": 13620.0
}, },
{ {
"x": [ "x": [
"e", "e",
"l" "l"
], ],
"y": "PT1H3M" "y": 3780.0
}, },
{ {
"x": [ "x": [
"f", "f",
"d" "d"
], ],
"y": "PT2H49M" "y": 10140.0
}, },
{ {
"x": [ "x": [
"f", "f",
"e" "e"
], ],
"y": "PT1H5M40.0S" "y": 3940.0
}, },
{ {
"x": [ "x": [
"f", "f",
"g" "g"
], ],
"y": "PT1H56M23.27S" "y": 6983.271028
}, },
{ {
"x": [ "x": [
"f", "f",
"j" "j"
], ],
"y": "PT2H16M10.91S" "y": 8170.909091
}, },
{ {
"x": [ "x": [
"f", "f",
"l" "l"
], ],
"y": "PT1H51M7.5S" "y": 6667.5
}, },
{ {
"x": [ "x": [
"g", "g",
"c" "c"
], ],
"y": "PT40M" "y": 2400.0
}, },
{ {
"x": [ "x": [
"g", "g",
"e" "e"
], ],
"y": "PT3H18M" "y": 11880.0
}, },
{ {
"x": [ "x": [
"g", "g",
"f" "f"
], ],
"y": "PT1H28M22.5S" "y": 5302.5
}, },
{ {
"x": [ "x": [
"g", "g",
"g" "g"
], ],
"y": "PT3H10M" "y": 11400.0
}, },
{ {
"x": [ "x": [
"g", "g",
"h" "h"
], ],
"y": "PT2H6M32.82S" "y": 7592.820513
}, },
{ {
"x": [ "x": [
"g", "g",
"i" "i"
], ],
"y": "PT1H59M" "y": 7140.0
}, },
{ {
"x": [ "x": [
"g", "g",
"k" "k"
], ],
"y": "PT2H15M16.0S" "y": 8116.0
}, },
{ {
"x": [ "x": [
"g", "g",
"l" "l"
], ],
"y": "PT2H4M17.37S" "y": 7457.368421
}, },
{ {
"x": [ "x": [
"h", "h",
"i" "i"
], ],
"y": "PT2H1M28.89S" "y": 7288.888889
}, },
{ {
"x": [ "x": [
"h", "h",
"l" "l"
], ],
"y": "PT1H56M" "y": 6960.0
}, },
{ {
"x": [ "x": [
"i", "i",
"a" "a"
], ],
"y": "PT2H28M30.0S" "y": 8910.0
}, },
{ {
"x": [ "x": [
"i", "i",
"b" "b"
], ],
"y": "PT1H38M" "y": 5880.0
}, },
{ {
"x": [ "x": [
"i", "i",
"c" "c"
], ],
"y": "PT1H31M" "y": 5460.0
}, },
{ {
"x": [ "x": [
"i", "i",
"d" "d"
], ],
"y": "PT2H8M30.45S" "y": 7710.447761
}, },
{ {
"x": [ "x": [
"i", "i",
"e" "e"
], ],
"y": "PT2H32M33.33S" "y": 9153.333333
}, },
{ {
"x": [ "x": [
"i", "i",
"g" "g"
], ],
"y": "PT2H24M" "y": 8640.0
}, },
{ {
"x": [ "x": [
"i", "i",
"i" "i"
], ],
"y": "PT1H45M24.0S" "y": 6324.0
}, },
{ {
"x": [ "x": [
"i", "i",
"k" "k"
], ],
"y": "PT54M" "y": 3240.0
}, },
{ {
"x": [ "x": [
"i", "i",
"l" "l"
], ],
"y": "PT1H59M48.75S" "y": 7188.75
} }
], ],
"x_axis": { "x_axis": {
@@ -794,8 +794,8 @@
] ]
}, },
"y_axis": { "y_axis": {
"min": "PT0S", "min": 0.0,
"max": "PT3H54M" "max": 14040.0
} }
} }
}, },
@@ -803,49 +803,49 @@
"cases": { "cases": {
"data": [ "data": [
{ {
"x": "2022-01-21T03:21:48Z", "x": "2022-01-21T03:21:48",
"y": 30 "y": 30
}, },
{ {
"x": "2022-02-26T08:13:24Z", "x": "2022-02-26T08:13:24",
"y": 25 "y": 25
}, },
{ {
"x": "2022-04-03T13:05:00Z", "x": "2022-04-03T13:05:00",
"y": 30 "y": 30
}, },
{ {
"x": "2022-05-09T17:56:36Z", "x": "2022-05-09T17:56:36",
"y": 26 "y": 26
}, },
{ {
"x": "2022-06-14T22:48:12Z", "x": "2022-06-14T22:48:12",
"y": 28 "y": 28
}, },
{ {
"x": "2022-07-21T03:39:48Z", "x": "2022-07-21T03:39:48",
"y": 27 "y": 27
}, },
{ {
"x": "2022-08-26T08:31:24Z", "x": "2022-08-26T08:31:24",
"y": 17 "y": 17
}, },
{ {
"x": "2022-10-01T13:23:00Z", "x": "2022-10-01T13:23:00",
"y": 24 "y": 24
}, },
{ {
"x": "2022-11-06T18:14:36Z", "x": "2022-11-06T18:14:36",
"y": 28 "y": 28
}, },
{ {
"x": "2022-12-12T23:06:12Z", "x": "2022-12-12T23:06:12",
"y": 17 "y": 17
} }
], ],
"x_axis": { "x_axis": {
"min": "2022-01-03T00:56:00Z", "min": "2022-01-03T00:56:00",
"max": "2022-12-31T01:32:00Z" "max": "2022-12-31T01:32:00"
}, },
"y_axis": { "y_axis": {
"min": 0, "min": 0,
@@ -855,51 +855,51 @@
"cases_by_task": { "cases_by_task": {
"data": [ "data": [
{ {
"x": "aT00:00:00Z", "x": "a",
"y": 184 "y": 184
}, },
{ {
"x": "bT00:00:00Z", "x": "b",
"y": 32 "y": 32
}, },
{ {
"x": "cT00:00:00Z", "x": "c",
"y": 48 "y": 48
}, },
{ {
"x": "dT00:00:00Z", "x": "d",
"y": 241 "y": 241
}, },
{ {
"x": "eT00:00:00Z", "x": "e",
"y": 249 "y": 249
}, },
{ {
"x": "fT00:00:00Z", "x": "f",
"y": 249 "y": 249
}, },
{ {
"x": "gT00:00:00Z", "x": "g",
"y": 250 "y": 250
}, },
{ {
"x": "hT00:00:00Z", "x": "h",
"y": 163 "y": 163
}, },
{ {
"x": "iT00:00:00Z", "x": "i",
"y": 175 "y": 175
}, },
{ {
"x": "jT00:00:00Z", "x": "j",
"y": 22 "y": 22
}, },
{ {
"x": "kT00:00:00Z", "x": "k",
"y": 48 "y": 48
}, },
{ {
"x": "lT00:00:00Z", "x": "l",
"y": 182 "y": 182
} }
], ],
+40 -40
View File
@@ -12,141 +12,141 @@
"cases": [ "cases": [
{ {
"id": "H00564053", "id": "H00564053",
"started_at": "2022-01-03T00:56:00Z", "started_at": "2022-01-03T00:56:00",
"completed_at": "2022-01-12T02:29:00Z", "completed_at": "2022-01-12T02:29:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00723931", "id": "H00723931",
"started_at": "2022-01-04T17:51:00Z", "started_at": "2022-01-04T17:51:00",
"completed_at": "2022-01-17T10:00:00Z", "completed_at": "2022-01-17T10:00:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00542949", "id": "H00542949",
"started_at": "2022-01-10T19:05:00Z", "started_at": "2022-01-10T19:05:00",
"completed_at": "2022-01-28T09:38:00Z", "completed_at": "2022-01-28T09:38:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00320575", "id": "H00320575",
"started_at": "2022-01-12T21:35:00Z", "started_at": "2022-01-12T21:35:00",
"completed_at": "2022-01-24T19:38:00Z", "completed_at": "2022-01-24T19:38:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00565387", "id": "H00565387",
"started_at": "2022-01-20T20:30:00Z", "started_at": "2022-01-20T20:30:00",
"completed_at": "2022-02-06T10:57:00Z", "completed_at": "2022-02-06T10:57:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00832338", "id": "H00832338",
"started_at": "2022-01-29T15:00:00Z", "started_at": "2022-01-29T15:00:00",
"completed_at": "2022-02-13T17:46:00Z", "completed_at": "2022-02-13T17:46:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00525137", "id": "H00525137",
"started_at": "2022-02-05T23:26:00Z", "started_at": "2022-02-05T23:26:00",
"completed_at": "2022-02-14T19:47:00Z", "completed_at": "2022-02-14T19:47:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00093124", "id": "H00093124",
"started_at": "2022-02-09T16:56:00Z", "started_at": "2022-02-09T16:56:00",
"completed_at": "2022-02-28T17:38:00Z", "completed_at": "2022-02-28T17:38:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00657586", "id": "H00657586",
"started_at": "2022-02-14T20:07:00Z", "started_at": "2022-02-14T20:07:00",
"completed_at": "2022-02-28T15:21:00Z", "completed_at": "2022-02-28T15:21:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00141668", "id": "H00141668",
"started_at": "2022-02-17T13:57:00Z", "started_at": "2022-02-17T13:57:00",
"completed_at": "2022-03-06T00:01:00Z", "completed_at": "2022-03-06T00:01:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00493818", "id": "H00493818",
"started_at": "2022-02-20T19:54:00Z", "started_at": "2022-02-20T19:54:00",
"completed_at": "2022-03-05T05:06:00Z", "completed_at": "2022-03-05T05:06:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00488827", "id": "H00488827",
"started_at": "2022-02-21T00:38:00Z", "started_at": "2022-02-21T00:38:00",
"completed_at": "2022-03-03T16:24:00Z", "completed_at": "2022-03-03T16:24:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00874806", "id": "H00874806",
"started_at": "2022-02-24T15:15:00Z", "started_at": "2022-02-24T15:15:00",
"completed_at": "2022-03-12T01:12:00Z", "completed_at": "2022-03-12T01:12:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00395448", "id": "H00395448",
"started_at": "2022-02-26T03:35:00Z", "started_at": "2022-02-26T03:35:00",
"completed_at": "2022-03-08T23:11:00Z", "completed_at": "2022-03-08T23:11:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00414605", "id": "H00414605",
"started_at": "2022-02-26T17:11:00Z", "started_at": "2022-02-26T17:11:00",
"completed_at": "2022-03-10T08:50:00Z", "completed_at": "2022-03-10T08:50:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00564269", "id": "H00564269",
"started_at": "2022-03-04T01:18:00Z", "started_at": "2022-03-04T01:18:00",
"completed_at": "2022-03-16T08:14:00Z", "completed_at": "2022-03-16T08:14:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00729845", "id": "H00729845",
"started_at": "2022-03-05T09:29:00Z", "started_at": "2022-03-05T09:29:00",
"completed_at": "2022-03-17T15:25:00Z", "completed_at": "2022-03-17T15:25:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00194115", "id": "H00194115",
"started_at": "2022-03-09T18:58:00Z", "started_at": "2022-03-09T18:58:00",
"completed_at": "2022-03-23T09:01:00Z", "completed_at": "2022-03-23T09:01:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00517238", "id": "H00517238",
"started_at": "2022-03-21T06:30:00Z", "started_at": "2022-03-21T06:30:00",
"completed_at": "2022-04-05T05:27:00Z", "completed_at": "2022-04-05T05:27:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
}, },
{ {
"id": "H00377237", "id": "H00377237",
"started_at": "2022-03-24T12:06:00Z", "started_at": "2022-03-24T12:06:00",
"completed_at": "2022-04-04T15:44:00Z", "completed_at": "2022-04-04T15:44:00",
"attributes": [], "attributes": [],
"facets": [] "facets": []
} }
+6 -7
View File
@@ -1,15 +1,14 @@
{ {
"username": "testadmin", "username": "testadmin",
"name": "Test Admin", "name": "Test Admin",
"is_admin": true,
"is_active": true, "is_active": true,
"is_sso": false, "is_sso": false,
"visits": 42, "has_data": true,
"visited_at": "2025-06-12T09:00:00Z",
"created_at": "2024-01-01T00:00:00Z",
"created_by": { "username": "system", "name": "System" },
"updated_at": "2025-06-10T14:30:00Z",
"updated_by": { "username": "testadmin", "name": "Test Admin" },
"roles": [ "roles": [
{ "code": "admin", "name": "Administrator" } { "code": "admin", "name": "Administrator" }
] ],
"detail": {
"visits": 42
}
} }
+23 -5
View File
@@ -6,8 +6,6 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module abbreviateNumber Duration abbreviation formatting. */ /** @module abbreviateNumber Duration abbreviation formatting. */
import formatDuration from "./formatDuration.js";
/** /**
* Converts a total number of seconds into a human-readable abbreviated * Converts a total number of seconds into a human-readable abbreviated
* duration string using days, hours, minutes, and seconds. * duration string using days, hours, minutes, and seconds.
@@ -17,7 +15,27 @@ import formatDuration from "./formatDuration.js";
* or "0" if totalSeconds is zero. * or "0" if totalSeconds is zero.
*/ */
export default function abbreviateNumber(totalSeconds) { export default function abbreviateNumber(totalSeconds) {
const parsed = Number.parseInt(totalSeconds); let seconds = 0;
if (parsed === 0) return "0"; let minutes = 0;
return formatDuration(parsed, { separator: " " }); let hours = 0;
let days = 0;
let result = "";
let symbols = ["d", "h", "m", "s"];
totalSeconds = Number.parseInt(totalSeconds);
if (!Number.isNaN(totalSeconds)) {
seconds = totalSeconds % 60;
minutes = (Math.floor(totalSeconds - seconds) / 60) % 60;
hours = Math.floor(totalSeconds / 3600) % 24;
days = Math.floor(totalSeconds / (3600 * 24));
}
const units = [days, hours, minutes, seconds];
for (let i = 0; i < units.length; i++) {
if (units[i] > 0) result += units[i] + symbols[i] + " ";
}
result = result.trim();
if (totalSeconds === 0) result = "0";
return result;
} }
+142 -116
View File
@@ -29,23 +29,20 @@ const customClass = {
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ", "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ",
}; };
/** /**
* Shows a modal dialog prompting for a name and saves through the * Shows a modal dialog to save a new filter with a user-provided name.
* given backend API function.
* *
* @param {string} title - The dialog title. * @param {Function} addFilterId - Backend API function to create the filter.
* @param {string} inputPlaceholder - The name input placeholder text. * @param {Function|null} [next=null] - Vue Router next() guard callback.
* @param {Function} save - Backend API function receiving the name. * @returns {Promise<boolean>} True if the filter was saved, false otherwise.
* @param {Function|null} [onCancel=null] - Callback invoked when the
* dialog is cancelled or dismissed.
* @returns {Promise<boolean>} True if saved, false otherwise.
*/ */
async function promptSaveName(title, inputPlaceholder, save, onCancel = null) { export async function saveFilter(addFilterId, next = null) {
let fileName = ""; let fileName = "";
const pageAdminStore = usePageAdminStore();
const { value, isConfirmed } = await Swal.fire({ const { value, isConfirmed } = await Swal.fire({
title: title, title: "SAVE NEW FILTER",
input: "text", input: "text",
inputPlaceholder: inputPlaceholder, inputPlaceholder: "Enter Filter Name.",
inputValue: fileName, inputValue: fileName,
inputAttributes: { inputAttributes: {
maxlength: 200, maxlength: 200,
@@ -67,7 +64,7 @@ async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
// Determine whether to redirect based on the return value // Determine whether to redirect based on the return value
if (isConfirmed) { if (isConfirmed) {
// Save succeeded // Save succeeded
await save(fileName); await addFilterId(fileName);
// Show save complete notification // Show save complete notification
if (value) { if (value) {
// Example of value: yes // Example of value: yes
@@ -76,34 +73,16 @@ async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
// Clear the input field // Clear the input field
fileName = ""; fileName = "";
return true; return true;
} } else {
// Clicked cancel or outside the dialog; save failed. // Clicked cancel or outside the dialog; save failed.
if (onCancel !== null) onCancel();
return false;
}
/**
* Shows a modal dialog to save a new filter with a user-provided name.
*
* @param {Function} addFilterId - Backend API function to create the filter.
* @param {Function|null} [next=null] - Vue Router next() guard callback.
* @returns {Promise<boolean>} True if the filter was saved, false otherwise.
*/
export async function saveFilter(addFilterId, next = null) {
const pageAdminStore = usePageAdminStore();
return promptSaveName(
"SAVE NEW FILTER",
"Enter Filter Name.",
addFilterId,
() => {
pageAdminStore.keepPreviousPage(); pageAdminStore.keepPreviousPage();
// Not every time we have nontrivial next value // Not every time we have nontrivial next value
if (next !== null) { if (next !== null) {
next(false); next(false);
} }
}, return false;
); }
} }
/** /**
* Shows a timed success notification after a file has been saved. * Shows a timed success notification after a file has been saved.
@@ -138,67 +117,9 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
const allMapDataStore = useAllMapDataStore(); const allMapDataStore = useAllMapDataStore();
const pageAdminStore = usePageAdminStore(); const pageAdminStore = usePageAdminStore();
await confirmLeave({ const result = await Swal.fire({
title: "SAVE YOUR FILTER?", title: "SAVE YOUR FILTER?",
html: "If you want to continue using this filter in any other page, please select [Yes].", html: "If you want to continue using this filter in any other page, please select [Yes].",
saveChanges: async () => {
if (allMapDataStore.createFilterId) {
await allMapDataStore.updateFilter();
if (allMapDataStore.isUpdateFilter) {
await savedSuccessfully(allMapDataStore.filterName);
}
} else {
// Dangerous, here shows a modal
await saveFilter(addFilterId, next);
}
},
navigateAfterSave: true,
// Handle page admin issue
beforeDismiss: () => pageAdminStore.keepPreviousPage(),
discardTemp: () => {
allMapDataStore.tempFilterId = null;
},
next: next,
toPath: toPath,
logOut: logOut,
});
}
/**
* Shows the "save before leaving" confirmation dialog and runs the
* confirm, cancel, and backdrop flows shared by the filter and
* conformance pages.
*
* @param {Object} options - The dialog and flow options.
* @param {string} options.title - The dialog title.
* @param {string} [options.html] - Optional dialog body HTML.
* @param {Function} options.saveChanges - Async callback saving the
* unsaved changes on confirm.
* @param {boolean} options.navigateAfterSave - Whether to navigate (or
* log out) after saving on confirm.
* @param {Function|null} options.beforeDismiss - Callback invoked on
* cancel or backdrop dismiss before anything else, or null.
* @param {Function} options.discardTemp - Callback discarding the
* temporary unsaved data on cancel.
* @param {Function} options.next - Vue Router next() guard callback.
* @param {string} options.toPath - The destination route path.
* @param {Function} [options.logOut] - Optional logout function to call
* instead of navigating.
* @returns {Promise<void>}
*/
async function confirmLeave({
title,
html,
saveChanges,
navigateAfterSave,
beforeDismiss,
discardTemp,
next,
toPath,
logOut,
}) {
const result = await Swal.fire({
title: title,
...(html !== undefined && { html: html }),
icon: "warning", icon: "warning",
iconColor: "#FF3366", iconColor: "#FF3366",
reverseButtons: true, reverseButtons: true,
@@ -211,16 +132,31 @@ async function confirmLeave({
}); });
if (result.isConfirmed) { if (result.isConfirmed) {
await saveChanges(); if (allMapDataStore.createFilterId) {
if (navigateAfterSave) { await allMapDataStore.updateFilter();
logOut ? logOut() : next(toPath); if (allMapDataStore.isUpdateFilter) {
await savedSuccessfully(allMapDataStore.filterName);
} }
} else {
// Dangerous, here shows a modal
await saveFilter(addFilterId, next);
}
logOut ? logOut() : next(toPath);
} else if (result.dismiss === "cancel") { } else if (result.dismiss === "cancel") {
if (beforeDismiss) beforeDismiss(); // console.log('popup cancel case', );
discardTemp(); // Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
pageAdminStore.keepPreviousPage();
allMapDataStore.tempFilterId = null;
logOut ? logOut() : next(toPath); logOut ? logOut() : next(toPath);
} else if (result.dismiss === "backdrop") { } else if (result.dismiss === "backdrop") {
if (beforeDismiss) beforeDismiss(); // console.log('popup backdrop case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
pageAdminStore.keepPreviousPage();
if (!logOut) { if (!logOut) {
next(false); next(false);
} }
@@ -235,16 +171,47 @@ async function confirmLeave({
* @returns {Promise<boolean>} True if the rule was saved, false otherwise. * @returns {Promise<boolean>} True if the rule was saved, false otherwise.
*/ */
export async function saveConformance(addConformanceCreateCheckId) { export async function saveConformance(addConformanceCreateCheckId) {
return promptSaveName( let fileName = "";
"SAVE NEW RULE", const { value, isConfirmed } = await Swal.fire({
"Enter Rule Name.", title: "SAVE NEW RULE",
addConformanceCreateCheckId, input: "text",
); inputPlaceholder: "Enter Rule Name.",
inputValue: fileName,
inputAttributes: {
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return "You need to write something!";
fileName = value;
},
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if (isConfirmed) {
// Save succeeded
await addConformanceCreateCheckId(fileName);
// Show save complete notification
if (value) savedSuccessfully(value);
// Clear the input field
fileName = "";
return true;
} else {
// Clicked cancel or outside the dialog; save failed.
return false;
}
} }
/** /**
* Prompts the user to save unsaved conformance rule changes before * Prompts the user to save unsaved conformance rule changes before
* leaving the Conformance page. Handles confirm (save), cancel * leaving the Conformance page. Delegates to helper functions for
* (discard), and backdrop (stay) scenarios. * confirm, cancel, and backdrop scenarios.
* *
* @param {Function} next - Vue Router next() guard callback. * @param {Function} next - Vue Router next() guard callback.
* @param {Function} addConformanceCreateCheckId - Backend API function * @param {Function} addConformanceCreateCheckId - Backend API function
@@ -260,10 +227,40 @@ export async function leaveConformance(
logOut, logOut,
) { ) {
const conformanceStore = useConformanceStore(); const conformanceStore = useConformanceStore();
const result = await showConfirmationDialog();
await confirmLeave({ if (result.isConfirmed) {
await handleConfirmed(conformanceStore, addConformanceCreateCheckId);
} else {
await handleDismiss(result.dismiss, conformanceStore, next, toPath, logOut);
}
}
/**
* Displays the "SAVE YOUR RULE?" confirmation dialog.
* @returns {Promise<Object>} The SweetAlert2 result object.
*/
async function showConfirmationDialog() {
return Swal.fire({
title: "SAVE YOUR RULE?", title: "SAVE YOUR RULE?",
saveChanges: async () => { icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
}
/**
* Handles the confirmed save action for conformance rules.
* @param {Object} conformanceStore - The conformance Pinia store.
* @param {Function} addConformanceCreateCheckId - API function to create check.
* @returns {Promise<void>}
*/
async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
if ( if (
conformanceStore.conformanceFilterCreateCheckId || conformanceStore.conformanceFilterCreateCheckId ||
conformanceStore.conformanceLogCreateCheckId conformanceStore.conformanceLogCreateCheckId
@@ -275,17 +272,46 @@ export async function leaveConformance(
} else { } else {
await saveConformance(addConformanceCreateCheckId); await saveConformance(addConformanceCreateCheckId);
} }
}, }
navigateAfterSave: false,
beforeDismiss: null, /**
discardTemp: () => { * Handles dismiss actions (cancel or backdrop click) for conformance modals.
* @param {string} dismissType - The SweetAlert2 dismiss reason.
* @param {Object} conformanceStore - The conformance Pinia store.
* @param {Function} next - Vue Router next() guard callback.
* @param {string} toPath - The destination route path.
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
async function handleDismiss(
dismissType,
conformanceStore,
next,
toPath,
logOut,
) {
switch (dismissType) {
case "cancel":
resetTempCheckId(conformanceStore);
logOut ? logOut() : next(toPath);
break;
case "backdrop":
if (!logOut) {
next(false);
}
break;
default:
break;
}
}
/**
* Resets temporary conformance check IDs to null.
* @param {Object} conformanceStore - The conformance Pinia store.
*/
function resetTempCheckId(conformanceStore) {
conformanceStore.conformanceFilterTempCheckId = null; conformanceStore.conformanceFilterTempCheckId = null;
conformanceStore.conformanceLogTempCheckId = null; conformanceStore.conformanceLogTempCheckId = null;
},
next: next,
toPath: toPath,
logOut: logOut,
});
} }
/** /**
+2 -1
View File
@@ -14,9 +14,10 @@ import "vue-toast-notification/dist/theme-sugar.css";
* 401 errors are handled by the axios response interceptor * 401 errors are handled by the axios response interceptor
* in api/client.js. * in api/client.js.
* *
* @param {Object} error - The error object from the API call.
* @param {string} toastMessage - The message to display in the toast. * @param {string} toastMessage - The message to display in the toast.
*/ */
export default function apiError(toastMessage) { export default function apiError(error, toastMessage) {
const $toast = useToast(); const $toast = useToast();
$toast.default(toastMessage, { position: "bottom" }); $toast.default(toastMessage, { position: "bottom" });
} }
-71
View File
@@ -1,71 +0,0 @@
// The Lucia project.
// Copyright 2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/7/5
// AI assistance: Claude Code (Anthropic)
/** @module formatDuration Shared duration formatting internals. */
/**
* The duration units from the largest to the smallest.
*
* @constant {Array<{symbol: string, seconds: number}>}
*/
export const DURATION_UNITS = [
{ symbol: "d", seconds: 24 * 60 * 60 },
{ symbol: "h", seconds: 60 * 60 },
{ symbol: "m", seconds: 60 },
{ symbol: "s", seconds: 1 },
];
/**
* Picks the largest duration unit suitable for a number of seconds.
*
* @param {number} seconds - The number of seconds to pick a unit for.
* @param {boolean} [exclusive=false] - Whether a unit is chosen only
* when the value is strictly greater than one unit (true), or when
* it is greater than or equal to one unit (false).
* @returns {{symbol: string, seconds: number}} The chosen unit; falls
* back to seconds when no larger unit qualifies.
*/
export function pickDurationUnit(seconds, exclusive = false) {
return DURATION_UNITS.find(
(unit) =>
unit.seconds === 1 ||
(exclusive ? seconds > unit.seconds : seconds >= unit.seconds),
);
}
/**
* Formats a duration in seconds as a day/hour/minute/second cascade.
*
* Zero-valued units are omitted, except that the seconds unit can be
* forced with the alwaysSeconds option. The result is truncated to at
* most maxUnits units, starting from the largest.
*
* @param {number} totalSeconds - The total number of seconds.
* @param {Object} [options] - The formatting options.
* @param {string} [options.separator=" "] - The string between units.
* @param {number} [options.maxUnits=Infinity] - The maximum number of
* units to keep, starting from the largest.
* @param {boolean} [options.alwaysSeconds=false] - Whether to always
* append the seconds unit, even when it is zero.
* @returns {string} The formatted duration (e.g. "2d 3h 15m 30s"), or
* an empty string when no unit qualifies.
*/
export default function formatDuration(
totalSeconds,
{ separator = " ", maxUnits = Infinity, alwaysSeconds = false } = {},
) {
const seconds = totalSeconds % 60;
const minutes = (Math.floor(totalSeconds - seconds) / 60) % 60;
const hours = Math.floor(totalSeconds / 3600) % 24;
const days = Math.floor(totalSeconds / (3600 * 24));
const parts = [];
if (days > 0) parts.push(days + "d");
if (hours > 0) parts.push(hours + "h");
if (minutes > 0) parts.push(minutes + "m");
if (seconds > 0 || alwaysSeconds) parts.push(seconds + "s");
return parts.slice(0, maxUnits).join(separator);
}
+17 -5
View File
@@ -6,18 +6,30 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module numberLabel Number formatting with comma separators. */ /** @module numberLabel Number formatting with comma separators. */
/**
* Formats an integer string by inserting commas every three digits.
* @param {string} numberStr - A string of digits to format.
* @returns {string} The formatted string with commas (e.g. "1,000,000").
*/
const formatNumberWithCommas = (numberStr) => {
let reversedStr = numberStr.split("").reverse().join("");
let groupedStr = reversedStr.match(/.{1,3}/g) || [];
let joinedStr = groupedStr.join(",");
let finalStr = joinedStr.split("").reverse().join("");
return finalStr;
};
/** /**
* Converts a number to a string with comma-separated thousands. * Converts a number to a string with comma-separated thousands.
* *
* Handles decimal numbers by formatting only the integer part. A comma * Handles decimal numbers by formatting only the integer part.
* is inserted before every group of three characters counted from the
* end of the integer part.
* *
* @param {number} num - The number to format. * @param {number} num - The number to format.
* @returns {string} The formatted number string (e.g. "1,234.56"). * @returns {string} The formatted number string (e.g. "1,234.56").
*/ */
export default function numberLabel(num) { export default function numberLabel(num) {
const parts = num.toString().split("."); let parts = num.toString().split(".");
parts[0] = parts[0].replace(/(.)(?=(?:.{3})+$)/g, "$1,"); parts[0] = formatNumberWithCommas(parts[0]);
return parts.join("."); return parts.join(".");
} }
+37 -22
View File
@@ -8,8 +8,6 @@
import getMoment from "moment"; import getMoment from "moment";
import formatDuration, { DURATION_UNITS } from "./formatDuration.js";
/** /**
* Extends backend chart data with extrapolated boundary points for * Extends backend chart data with extrapolated boundary points for
* line charts. Prepends a calculated minimum and appends a calculated * line charts. Prepends a calculated minimum and appends a calculated
@@ -243,8 +241,26 @@ export function getXIndex(data, xValue) {
export function formatTime(seconds) { export function formatTime(seconds) {
if (Number.isNaN(Number(seconds))) { if (Number.isNaN(Number(seconds))) {
return null; return null;
} else {
const remainingSeconds = seconds % 60;
const minutes = (Math.floor(seconds - remainingSeconds) / 60) % 60;
const hours = Math.floor(seconds / 3600) % 24;
const days = Math.floor(seconds / (3600 * 24));
let result = "";
if (days > 0) {
result += `${days}d`;
}
if (hours > 0) {
result += `${hours}h`;
}
if (minutes > 0) {
result += `${minutes}m`;
}
result += `${remainingSeconds}s`;
return result.trim(); // Remove trailing whitespace
} }
return formatDuration(seconds, { separator: "", alwaysSeconds: true });
} }
/** /**
* Truncates each time string to show only the two largest time units. * Truncates each time string to show only the two largest time units.
@@ -253,23 +269,22 @@ export function formatTime(seconds) {
* @returns {Array<string>} Array of truncated strings (e.g. "2d 3h"). * @returns {Array<string>} Array of truncated strings (e.g. "2d 3h").
*/ */
export function formatMaxTwo(times) { export function formatMaxTwo(times) {
const secondsPerUnit = Object.fromEntries( const formattedTimes = [];
DURATION_UNITS.map((unit) => [unit.symbol, unit.seconds]), for (let time of times) {
); // Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
return times.map((time) => { let units = time.match(/\d{1,10}[dhms]/g) || [];
// Match numbers and units (days, hours, minutes, seconds); assume let formattedTime = "";
// numbers have at most 10 digits. let count = 0;
const units = time.match(/\d{1,10}[dhms]/g);
if (!units) return ""; // Keep only the two largest units
const totalSeconds = units.reduce( for (let unit of units) {
(sum, unit) => if (count >= 2) {
sum + Number.parseInt(unit) * secondsPerUnit[unit.slice(-1)], break;
0, }
); formattedTime += unit + " ";
return formatDuration(totalSeconds, { count++;
separator: " ", }
maxUnits: 2, formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
alwaysSeconds: units.some((unit) => unit.endsWith("s")), }
}); return formattedTimes;
});
} }
+11 -1
View File
@@ -16,7 +16,17 @@
* @returns {Array<string|number>} The sorted array (same reference). * @returns {Array<string|number>} The sorted array (same reference).
*/ */
export function sortNumEngZhtw(data) { export function sortNumEngZhtw(data) {
return data.sort(sortNumEngZhtwForFilter); return data.sort((a, b) => {
const isANumber = !Number.isNaN(Number.parseFloat(a)) && Number.isFinite(Number(a));
const isBNumber = !Number.isNaN(Number.parseFloat(b)) && Number.isFinite(Number(b));
if (isANumber && isBNumber) return Number.parseFloat(a) - Number.parseFloat(b);
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
});
} }
/** /**
+111 -41
View File
@@ -8,8 +8,6 @@
import moment from "moment"; import moment from "moment";
import { pickDurationUnit } from "./formatDuration.js";
/** @constant {number} Number of decimal places for formatted time values. */ /** @constant {number} Number of decimal places for formatted time values. */
const TOFIXED_DECIMAL = 1; const TOFIXED_DECIMAL = 1;
@@ -38,11 +36,34 @@ export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
* character ("d", "h", "m", or "s") and the converted time value. * character ("d", "h", "m", or "s") and the converted time value.
*/ */
const getTimeUnitAndValueToUse = (secondToDecide) => { const getTimeUnitAndValueToUse = (secondToDecide) => {
const unit = pickDurationUnit(secondToDecide, true); const day = 24 * 60 * 60;
const hour = 60 * 60;
const minutes = 60;
const dd = secondToDecide / day;
const hh = secondToDecide / hour;
const mm = secondToDecide / minutes;
if (dd > 0 && dd > 1) {
return { return {
unitToUse: unit.symbol, unitToUse: "d",
timeValue: secondToDecide / unit.seconds, timeValue: secondToDecide / day,
}; };
} else if (hh > 0 && hh > 1) {
return {
unitToUse: "h",
timeValue: secondToDecide / hour,
};
} else if (mm > 0 && mm > 1) {
return {
unitToUse: "m",
timeValue: secondToDecide / minutes,
};
} else {
return {
unitToUse: "s",
timeValue: secondToDecide,
};
}
}; };
/** /**
@@ -59,27 +80,6 @@ export function getYTicksByIndex(stepSize, index, unitToUse) {
return `${shortenStepsizeMultIndex}${unitToUse}`; return `${shortenStepsizeMultIndex}${unitToUse}`;
} }
/**
* Formats seconds as a single-unit time label with the given suffixes.
*
* Chooses the largest unit whose size the value reaches. Values are
* rounded to fixedNumber decimal places, except seconds, which are
* concatenated as-is.
*
* @param {number} second - The total number of seconds.
* @param {number} fixedNumber - Number of decimal places.
* @param {{d: string, h: string, m: string, s: string}} suffixes - The
* unit suffixes to append.
* @returns {string} The formatted time label.
*/
function scaledTimeLabel(second, fixedNumber, suffixes) {
const unit = pickDurationUnit(second);
if (unit.symbol === "s") {
return second + suffixes.s;
}
return (second / unit.seconds).toFixed(fixedNumber) + suffixes[unit.symbol];
}
/** /**
* Converts seconds to a human-readable time string with full unit names. * Converts seconds to a human-readable time string with full unit names.
* *
@@ -89,12 +89,27 @@ function scaledTimeLabel(second, fixedNumber, suffixes) {
* "30 mins", "5 sec"). * "30 mins", "5 sec").
*/ */
export function getTimeLabel(second, fixedNumber = 0) { export function getTimeLabel(second, fixedNumber = 0) {
return scaledTimeLabel(second, fixedNumber, { const day = 24 * 60 * 60;
d: " days", const hour = 60 * 60;
h: " hrs", const minutes = 60;
m: " mins",
s: " sec", // The modulo operation limits the value to within one day (0 to 86399),
}); // representing the remaining seconds after full days.
const dd = Math.floor(second / day);
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + " days";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
if (second === 0) {
return second + " sec";
}
return second + " sec";
} }
/** /**
@@ -108,12 +123,24 @@ export function getTimeLabel(second, fixedNumber = 0) {
* @returns {string} The formatted string (e.g. "1d", "6.8h", "30m", "5s"). * @returns {string} The formatted string (e.g. "1d", "6.8h", "30m", "5s").
*/ */
export function simpleTimeLabel(second, fixedNumber = 0) { export function simpleTimeLabel(second, fixedNumber = 0) {
return scaledTimeLabel(second, fixedNumber, { const day = 24 * 60 * 60;
d: "d", const hour = 60 * 60;
h: "h", const minutes = 60;
m: "m", const dd = Math.floor(second / day);
s: "s", const hh = Math.floor((second % day) / hour);
}); const mm = Math.floor((second % hour) / minutes);
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + "d";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + "h";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
if (second === 0) {
return second + "s";
}
return second + "s";
} }
/** /**
* Converts seconds to a time string using the same unit as the maximum * Converts seconds to a time string using the same unit as the maximum
@@ -128,9 +155,52 @@ export function simpleTimeLabel(second, fixedNumber = 0) {
* @returns {string} The formatted string (e.g. "1.5d", "6.8h"). * @returns {string} The formatted string (e.g. "1.5d", "6.8h").
*/ */
export function followTimeLabel(second, max, fixedNumber = 0) { export function followTimeLabel(second, max, fixedNumber = 0) {
const unit = pickDurationUnit(max, true); const day = 24 * 60 * 60;
const value = second / unit.seconds; const hour = 60 * 60;
return value.toFixed(value === 0 ? 0 : fixedNumber) + unit.symbol; const minutes = 60;
const dd = max / day;
const hh = max / hour;
const mm = max / minutes;
let maxUnit = "";
let result = "";
if (dd > 1) {
maxUnit = "d";
} else if (hh > 1) {
maxUnit = "h";
} else if (mm > 1) {
maxUnit = "m";
} else {
maxUnit = "s";
}
switch (maxUnit) {
case "d":
if (second / day === 0) {
fixedNumber = 0;
}
result = (second / day).toFixed(fixedNumber) + "d";
break;
case "h":
if (second / hour === 0) {
fixedNumber = 0;
}
result = (second / hour).toFixed(fixedNumber) + "h";
break;
case "m":
if (second / minutes === 0) {
fixedNumber = 0;
}
result = (second / minutes).toFixed(fixedNumber) + "m";
break;
case "s":
if (second === 0) {
fixedNumber = 0;
}
result = second.toFixed(fixedNumber) + "s";
break;
}
return result;
} }
/** /**
+8 -8
View File
@@ -103,7 +103,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
const customizedResponseData = await this.customizeAllUserList(response.data); const customizedResponseData = await this.customizeAllUserList(response.data);
this.allUserAccountList = await this.moveCurrentLoginUserToFirstRow(customizedResponseData); this.allUserAccountList = await this.moveCurrentLoginUserToFirstRow(customizedResponseData);
} catch (error) { } catch (error) {
apiError('Failed to get all users.'); apiError(error, 'Failed to get all users.');
} }
}, },
/** /**
@@ -155,7 +155,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
setTimeout(() => this.resetJustCreateFlag(), JUST_CREATE_ACCOUNT_HOT_DURATION_MINS * 1000 * 60); setTimeout(() => this.resetJustCreateFlag(), JUST_CREATE_ACCOUNT_HOT_DURATION_MINS * 1000 * 60);
} }
} catch (error) { } catch (error) {
apiError('Failed to add a new account.'); apiError(error, 'Failed to add a new account.');
} }
}, },
/** /**
@@ -170,7 +170,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
const response = await apiClient.delete(apiDelete); const response = await apiClient.delete(apiDelete);
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to delete the account.'); apiError(error, 'Failed to delete the account.');
return false; return false;
} }
}, },
@@ -191,7 +191,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
}); });
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to edit the account.'); apiError(error, 'Failed to edit the account.');
return false; return false;
} }
}, },
@@ -207,7 +207,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
}); });
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to edit name of account.'); apiError(error, 'Failed to edit name of account.');
return false; return false;
} }
}, },
@@ -224,7 +224,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
}); });
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to edit password of account.'); apiError(error, 'Failed to edit password of account.');
return false; return false;
} }
}, },
@@ -239,7 +239,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
const response = await apiClient.put(apiAddRole); const response = await apiClient.put(apiAddRole);
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to add role to the account.'); apiError(error, 'Failed to add role to the account.');
return false; return false;
} }
}, },
@@ -254,7 +254,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
const response = await apiClient.delete(apiDeleteRole); const response = await apiClient.delete(apiDeleteRole);
return response.status === 200; return response.status === 200;
} catch (error) { } catch (error) {
apiError('Failed to delete a role from the account.'); apiError(error, 'Failed to delete a role from the account.');
return false; return false;
} }
}, },
+52 -10
View File
@@ -71,18 +71,57 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
processMap: (state) => { processMap: (state) => {
return state.allProcessMap; return state.allProcessMap;
}, },
bpmn: (state) => {
return state.allBpmn;
},
stats: (state) => {
return state.allStats;
},
insights: (state) => {
return state.allInsights;
},
traces: (state) => { traces: (state) => {
return [...state.allTrace].sort((x, y) => x.id - y.id); return [...state.allTrace].sort((x, y) => x.id - y.id);
}, },
baseTraces: (state) => { baseTraces: (state) => {
return [...state.allBaseTrace].sort((x, y) => x.id - y.id); return [...state.allBaseTrace].sort((x, y) => x.id - y.id);
}, },
cases: (state) => {
return state.allCase;
},
baseCases: (state) => {
return state.allBaseCase;
},
infiniteFirstCases: (state) => { infiniteFirstCases: (state) => {
if (state.infiniteStart === 0) return state.allCase; if (state.infiniteStart === 0) return state.allCase;
}, },
BaseInfiniteFirstCases: (state) => { BaseInfiniteFirstCases: (state) => {
if (state.baseInfiniteStart === 0) return state.allBaseCase; if (state.baseInfiniteStart === 0) return state.allBaseCase;
}, },
traceTaskSeq: (state) => {
return state.allTraceTaskSeq;
},
baseTraceTaskSeq: (state) => {
return state.allBaseTraceTaskSeq;
},
// All tasks
filterTasks: (state) => {
return state.allFilterTask;
},
// form start to end tasks
filterStartToEnd: (state) => {
return state.allFilterStartToEnd;
},
// form end to start tasks
filterEndToStart: (state) => {
return state.allFilterEndToStart;
},
filterTimeframe: (state) => {
return state.allFilterTimeframe;
},
filterTrace: (state) => {
return state.allFilterTrace;
},
filterAttrs: (state) => { filterAttrs: (state) => {
if (state.allFilterAttrs !== null) { if (state.allFilterAttrs !== null) {
return state.allFilterAttrs.map((att) => { return state.allFilterAttrs.map((att) => {
@@ -116,6 +155,9 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
} }
return []; return [];
}, },
allFunnels: (state) => {
return state.allFunnelData;
},
}, },
actions: { actions: {
/** /**
@@ -130,7 +172,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.allStats = response.data.stats; this.allStats = response.data.stats;
this.allInsights = response.data.insights; this.allInsights = response.data.insights;
} catch (error) { } catch (error) {
apiError("Failed to load the Map."); apiError(error, "Failed to load the Map.");
} }
}, },
/** /**
@@ -149,7 +191,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.allBaseTrace = baseResponse.data; this.allBaseTrace = baseResponse.data;
} }
} catch (error) { } catch (error) {
apiError("Failed to load the Trace."); apiError(error, "Failed to load the Trace.");
} }
}, },
/** /**
@@ -191,7 +233,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.infinite404 = 404; this.infinite404 = 404;
return; return;
} }
apiError("Failed to load the Trace Detail."); apiError(error, "Failed to load the Trace Detail.");
return []; return [];
} }
}, },
@@ -236,7 +278,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.infinite404 = 404; this.infinite404 = 404;
return; return;
} }
apiError("Failed to load the Base Trace Detail."); apiError(error, "Failed to load the Base Trace Detail.");
return []; return [];
} }
}, },
@@ -267,7 +309,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.allFilterTimeframe.x_axis.max = this.allFilterTimeframe.x_axis.max =
max === null ? null : moment(max).format("YYYY/MM/DD HH:mm"); max === null ? null : moment(max).format("YYYY/MM/DD HH:mm");
} catch (error) { } catch (error) {
apiError("Failed to load the Filter Parameters."); apiError(error, "Failed to load the Filter Parameters.");
} }
}, },
/** /**
@@ -281,7 +323,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
const response = await apiClient.post(api, this.postRuleData); const response = await apiClient.post(api, this.postRuleData);
this.hasResultRule = response.data.result; this.hasResultRule = response.data.result;
} catch (error) { } catch (error) {
apiError("Failed to load the Has Result."); apiError(error, "Failed to load the Has Result.");
} }
}, },
/** /**
@@ -295,7 +337,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
const response = await apiClient.post(api, this.postRuleData); const response = await apiClient.post(api, this.postRuleData);
this.tempFilterId = response.data.id; this.tempFilterId = response.data.id;
} catch (error) { } catch (error) {
apiError("Failed to add the Temporary Filters."); apiError(error, "Failed to add the Temporary Filters.");
} }
}, },
/** /**
@@ -315,7 +357,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.createFilterId = response.data.id; this.createFilterId = response.data.id;
this.tempFilterId = null; this.tempFilterId = null;
} catch (error) { } catch (error) {
apiError("Failed to load the Filters."); apiError(error, "Failed to load the Filters.");
} }
}, },
/** /**
@@ -333,7 +375,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.filterName = response.data.name; this.filterName = response.data.name;
this.baseLogId = response.data.log?.id; this.baseLogId = response.data.log?.id;
} catch (error) { } catch (error) {
apiError("Failed to get Filter Detail."); apiError(error, "Failed to get Filter Detail.");
} }
} }
}, },
@@ -350,7 +392,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
this.isUpdateFilter = response.status === 200; this.isUpdateFilter = response.status === 200;
this.tempFilterId = null; this.tempFilterId = null;
} catch (error) { } catch (error) {
apiError("Failed to update an Existing Filter."); apiError(error, "Failed to update an Existing Filter.");
} }
}, },
}, },
+3 -3
View File
@@ -37,7 +37,7 @@ export const useCompareStore = defineStore("compareStore", {
const response = await apiClient.get(api); const response = await apiClient.get(api);
this.allCompareDashboardData = response.data; this.allCompareDashboardData = response.data;
} catch (error) { } catch (error) {
apiError("Failed to load the Compare."); apiError(error, "Failed to load the Compare.");
} }
}, },
/** /**
@@ -63,7 +63,7 @@ export const useCompareStore = defineStore("compareStore", {
return response.data.stats; return response.data.stats;
} catch (error) { } catch (error) {
apiError("Failed to load the Compare's States."); apiError(error, "Failed to load the Compare's States.");
return null; return null;
} }
}, },
@@ -79,7 +79,7 @@ export const useCompareStore = defineStore("compareStore", {
if (file) return file.name; if (file) return file.name;
} catch (error) { } catch (error) {
apiError("Failed to load the Compare's file name."); apiError(error, "Failed to load the Compare's file name.");
} }
return ""; return "";
}, },
+36 -5
View File
@@ -91,9 +91,18 @@ export const useConformanceStore = defineStore("conformanceStore", {
conformanceFileName: null, // File name displayed in the save success modal conformanceFileName: null, // File name displayed in the save success modal
}), }),
getters: { getters: {
conformanceAllTasks: (state) => {
return state.allConformanceTask;
},
conformanceTask: (state) => { conformanceTask: (state) => {
return state.allConformanceTask.map((i) => i.label); return state.allConformanceTask.map((i) => i.label);
}, },
cfmSeqStart: (state) => {
return state.allCfmSeqStart;
},
cfmSeqEnd: (state) => {
return state.allCfmSeqEnd;
},
cfmPtEteWhole: (state) => { cfmPtEteWhole: (state) => {
return state.allProcessingTime.end_to_end.whole; return state.allProcessingTime.end_to_end.whole;
}, },
@@ -148,6 +157,18 @@ export const useConformanceStore = defineStore("conformanceStore", {
cfmCtEteSE: (state) => { cfmCtEteSE: (state) => {
return state.allCycleTime.end_to_end.start_end; return state.allCycleTime.end_to_end.start_end;
}, },
conformanceTempReportData: (state) => {
return state.allConformanceTempReportData;
},
routeFile: (state) => {
return state.allRouteFile;
},
issueTraces: (state) => {
return state.allIssueTraces;
},
taskSeq: (state) => {
return state.allTaskSeq;
},
cases: (state) => { cases: (state) => {
if (state.allCases !== null) { if (state.allCases !== null) {
return state.allCases.map((c) => { return state.allCases.map((c) => {
@@ -199,6 +220,12 @@ export const useConformanceStore = defineStore("conformanceStore", {
} }
return []; return [];
}, },
loopTraces: (state) => {
return state.allLoopTraces;
},
loopTaskSeq: (state) => {
return state.allLoopTaskSeq;
},
loopCases: (state) => { loopCases: (state) => {
if (state.allLoopCases !== null) { if (state.allLoopCases !== null) {
return state.allLoopCases.map((c) => { return state.allLoopCases.map((c) => {
@@ -249,7 +276,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.allWaitingTime = response.data.waiting_time; this.allWaitingTime = response.data.waiting_time;
this.allCycleTime = response.data.cycle_time; this.allCycleTime = response.data.cycle_time;
} catch (error) { } catch (error) {
apiError("Failed to load the Conformance Parameters."); apiError(error, "Failed to load the Conformance Parameters.");
} }
}, },
/** /**
@@ -268,7 +295,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.conformanceLogTempCheckId = response.data.id; this.conformanceLogTempCheckId = response.data.id;
} }
} catch (error) { } catch (error) {
apiError("Failed to add the Temporary Check for a file."); apiError(error, "Failed to add the Temporary Check for a file.");
} }
}, },
/** /**
@@ -285,7 +312,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.allConformanceTempReportData = response.data; this.allConformanceTempReportData = response.data;
} }
} catch (error) { } catch (error) {
apiError("Failed to Get the Temporary Log Conformance Report."); apiError(error, "Failed to Get the Temporary Log Conformance Report.");
} }
}, },
/** /**
@@ -299,6 +326,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.allIssueTraces = response.data.traces; this.allIssueTraces = response.data.traces;
} catch (error) { } catch (error) {
apiError( apiError(
error,
"Failed to Get the detail of a temporary log conformance issue.", "Failed to Get the detail of a temporary log conformance issue.",
); );
} }
@@ -322,6 +350,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
return null; return null;
} }
apiError( apiError(
error,
"Failed to Get the detail of a temporary log conformance issue.", "Failed to Get the detail of a temporary log conformance issue.",
); );
return null; return null;
@@ -338,6 +367,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.allLoopTraces = response.data.traces; this.allLoopTraces = response.data.traces;
} catch (error) { } catch (error) {
apiError( apiError(
error,
"Failed to Get the detail of a temporary log conformance loop.", "Failed to Get the detail of a temporary log conformance loop.",
); );
} }
@@ -361,6 +391,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
return null; return null;
} }
apiError( apiError(
error,
"Failed to Get the detail of a temporary log conformance loop.", "Failed to Get the detail of a temporary log conformance loop.",
); );
return null; return null;
@@ -388,7 +419,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.conformanceLogTempCheckId = null; this.conformanceLogTempCheckId = null;
} }
} catch (error) { } catch (error) {
apiError("Failed to add the Conformance Check for a file."); apiError(error, "Failed to add the Conformance Check for a file.");
} }
}, },
/** /**
@@ -404,7 +435,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
this.conformanceLogTempCheckId = null; this.conformanceLogTempCheckId = null;
this.conformanceFilterTempCheckId = null; this.conformanceFilterTempCheckId = null;
} catch (error) { } catch (error) {
apiError("Failed to update an Existing Conformance."); apiError(error, "Failed to update an Existing Conformance.");
} }
}, },
/** /**
+8 -2
View File
@@ -26,8 +26,8 @@ export interface Node {
} }
export interface NodePositions { export interface NodePositions {
[graphId: string]: { [direction: string]: {
[direction: string]: Node[]; [graphId: string]: Node[];
} }
} }
@@ -51,6 +51,8 @@ export const useCytoscapeStore = defineStore('cytoscapeStore', {
if (!this.nodePositions[this.currentGraphId][direction]) { if (!this.nodePositions[this.currentGraphId][direction]) {
this.nodePositions[this.currentGraphId][direction] = []; this.nodePositions[this.currentGraphId][direction] = [];
} }
// If this graph's data was previously stored in localStorage
if (localStorage.getItem(SAVE_KEY_NAME)) {
const nodeToSave = this.nodePositions[this.currentGraphId][direction] const nodeToSave = this.nodePositions[this.currentGraphId][direction]
.find(node => node.id === nodeId); .find(node => node.id === nodeId);
if (nodeToSave) { if (nodeToSave) {
@@ -59,6 +61,10 @@ export const useCytoscapeStore = defineStore('cytoscapeStore', {
this.nodePositions[this.currentGraphId][direction] this.nodePositions[this.currentGraphId][direction]
.push({ id: nodeId, position: position }); .push({ id: nodeId, position: position });
} }
} else {
this.nodePositions[this.currentGraphId][direction]
.push({ id: nodeId, position: position });
}
this.savePositionsToStorage(); this.savePositionsToStorage();
}, },
/** /**
+9 -9
View File
@@ -146,7 +146,7 @@ export const useFilesStore = defineStore("filesStore", {
: null; : null;
}); });
} catch (error) { } catch (error) {
apiError("Failed to load the files."); apiError(error, "Failed to load the files.");
} }
}, },
/** /**
@@ -180,7 +180,7 @@ export const useFilesStore = defineStore("filesStore", {
} }
} else { } else {
Swal.close(); // Close the loading progress bar Swal.close(); // Close the loading progress bar
apiError("Failed to upload the files."); apiError(error, "Failed to upload the files.");
} }
} }
}, },
@@ -195,7 +195,7 @@ export const useFilesStore = defineStore("filesStore", {
const response = await apiClient.get(api); const response = await apiClient.get(api);
this.allUploadDetail = response.data.preview; this.allUploadDetail = response.data.preview;
} catch (error) { } catch (error) {
apiError("Failed to get upload detail."); apiError(error, "Failed to get upload detail.");
} }
}, },
/** /**
@@ -223,7 +223,7 @@ export const useFilesStore = defineStore("filesStore", {
uploadFailedSecond(detail); uploadFailedSecond(detail);
} else { } else {
Swal.close(); // Close the loading progress bar Swal.close(); // Close the loading progress bar
apiError("Failed to upload the log files."); apiError(error, "Failed to upload the log files.");
} }
} }
}, },
@@ -247,7 +247,7 @@ export const useFilesStore = defineStore("filesStore", {
this.uploadFileName = null; this.uploadFileName = null;
await this.fetchAllFiles(); await this.fetchAllFiles();
} catch (error) { } catch (error) {
apiError("Failed to rename."); apiError(error, "Failed to rename.");
} }
}, },
/** /**
@@ -262,7 +262,7 @@ export const useFilesStore = defineStore("filesStore", {
); );
this.allDependentsData = response.data; this.allDependentsData = response.data;
} catch (error) { } catch (error) {
apiError("Failed to get Dependents of the files."); apiError(error, "Failed to get Dependents of the files.");
} }
}, },
/** /**
@@ -282,7 +282,7 @@ export const useFilesStore = defineStore("filesStore", {
await this.fetchAllFiles(); await this.fetchAllFiles();
await deleteSuccess(); await deleteSuccess();
} catch (error) { } catch (error) {
apiError("Failed to delete."); apiError(error, "Failed to delete.");
} finally { } finally {
loading.isLoading = false; loading.isLoading = false;
} }
@@ -300,7 +300,7 @@ export const useFilesStore = defineStore("filesStore", {
try { try {
await apiClient.delete(api); await apiClient.delete(api);
} catch (error) { } catch (error) {
apiError("Failed to Remove a Deletion Record."); apiError(error, "Failed to Remove a Deletion Record.");
} finally { } finally {
loading.isLoading = false; loading.isLoading = false;
} }
@@ -325,7 +325,7 @@ export const useFilesStore = defineStore("filesStore", {
link.click(); link.click();
globalThis.URL.revokeObjectURL(url); globalThis.URL.revokeObjectURL(url);
} catch (error) { } catch (error) {
apiError("Failed to download."); apiError(error, "Failed to download.");
} }
}, },
}, },
+1 -1
View File
@@ -121,7 +121,7 @@ export const useLoginStore = defineStore("loginStore", {
this.userData = response.data; this.userData = response.data;
} catch (error) { } catch (error) {
apiError("Failed to load user data."); apiError(error, "Failed to load user data.");
} }
}, },
/** /**
+3 -1
View File
@@ -5,11 +5,13 @@
// imacat.yang@dsp.im (imacat), 2023/9/23 // imacat.yang@dsp.im (imacat), 2023/9/23
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** /**
* @module stores/main Pinia store instance. * @module stores/main Pinia store instance with persisted state plugin.
*/ */
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export default pinia; export default pinia;
+4 -4
View File
@@ -163,8 +163,8 @@ export const useMapPathStore = defineStore('mapPathStore', {
} }
}, },
async createInsightWithPath() { async createInsightWithPath() {
const { allInsights } = useAllMapDataStore(); const { insights } = useAllMapDataStore();
this.insights = { ...allInsights }; this.insights = { ...insights };
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes() this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
.filter(function (elem) { .filter(function (elem) {
return elem.data('label').toLowerCase() === 'start'; return elem.data('label').toLowerCase() === 'start';
@@ -182,8 +182,8 @@ export const useMapPathStore = defineStore('mapPathStore', {
}); });
// Depth First Search from the starting node // Depth First Search from the starting node
this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []); this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []);
const { allInsights } = useAllMapDataStore(); const { insights } = useAllMapDataStore();
this.insights = { ...allInsights }; this.insights = { ...insights };
this.matchGraphPathWithInsightsPath(); this.matchGraphPathWithInsightsPath();
}, },
/** /**
+1 -1
View File
@@ -52,7 +52,7 @@ export const usePerformanceStore = defineStore("performanceStore", {
const response = await apiClient.get(api); const response = await apiClient.get(api);
this.allPerformanceData = response.data; this.allPerformanceData = response.data;
} catch (error) { } catch (error) {
apiError("Failed to load the Performance."); apiError(error, "Failed to load the Performance.");
} }
}, },
/** /**
+39
View File
@@ -0,0 +1,39 @@
// 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
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module jsUtils General JavaScript utility functions. */
/**
* Recursively prints an object's properties to the console.
* @param {Object} obj - The object to print.
* @param {number} [indent=0] - The current indentation level.
*/
export const printObject = (obj, indent = 0) => {
const padding = " ".repeat(indent);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === "object" && obj[key] !== null) {
console.log(`${padding}${key}: {`);
printObject(obj[key], indent + 2);
console.log(`${padding}}`);
} else {
console.log(`${padding}${key}: ${obj[key]}`);
}
}
}
};
/**
* Returns a cryptographically random integer between 0 and max (inclusive).
* @param {number} max - The upper bound (inclusive).
* @returns {number} A random integer in [0, max].
*/
export const getRandomInt = (max) => {
const array = new Uint32Array(1);
globalThis.crypto.getRandomValues(array);
return Math.floor((array[0] / (0xffffffff + 1)) * (max + 1));
};
@@ -63,6 +63,6 @@ const is_active = computed(() => currentViewingUser.value.is_active);
onBeforeMount(async () => { onBeforeMount(async () => {
await acctMgmtStore.getUserDetail(currentViewingUser.value.username); await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
visitTime.value = currentViewingUser.value.visits ?? 0; visitTime.value = currentViewingUser.value.detail?.visits ?? 0;
}); });
</script> </script>
+21 -21
View File
@@ -86,23 +86,23 @@
></SidebarView> ></SidebarView>
<SidebarState <SidebarState
v-model:visible="sidebarState" v-model:visible="sidebarState"
:insights="allInsights" :insights="insights"
:stats="allStats" :stats="stats"
></SidebarState> ></SidebarState>
<SidebarTraces <SidebarTraces
v-model:visible="sidebarTraces" v-model:visible="sidebarTraces"
:cases="allCase" :cases="cases"
@switch-Trace-Id="switchTraceId" @switch-Trace-Id="switchTraceId"
ref="tracesViewRef" ref="tracesViewRef"
> >
</SidebarTraces> </SidebarTraces>
<SidebarFilter <SidebarFilter
v-model:visible="sidebarFilter" v-model:visible="sidebarFilter"
:filterTasks="allFilterTask" :filterTasks="filterTasks"
:filterStartToEnd="allFilterStartToEnd" :filterStartToEnd="filterStartToEnd"
:filterEndToStart="allFilterEndToStart" :filterEndToStart="filterEndToStart"
:filterTimeframe="allFilterTimeframe" :filterTimeframe="filterTimeframe"
:filterTrace="allFilterTrace" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @submit-all="createCy(mapType)"
@switch-Trace-Id="switchTraceId" @switch-Trace-Id="switchTraceId"
ref="sidebarFilterRefComp" ref="sidebarFilterRefComp"
@@ -138,7 +138,7 @@ export default {
break; break;
} }
await conformanceStore.getConformanceReport(true); await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.allRouteFile; to.meta.file = conformanceStore.routeFile;
} }
next(); next();
}, },
@@ -176,25 +176,25 @@ const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore); const { isLoading } = storeToRefs(loadingStore);
const { const {
processMap, processMap,
allBpmn, bpmn,
allStats, stats,
allInsights, insights,
traceId, traceId,
traces, traces,
baseTraces, baseTraces,
baseTraceId, baseTraceId,
allFilterTask, filterTasks,
allFilterStartToEnd, filterStartToEnd,
allFilterEndToStart, filterEndToStart,
allFilterTimeframe, filterTimeframe,
allFilterTrace, filterTrace,
temporaryData, temporaryData,
isRuleData, isRuleData,
ruleData, ruleData,
logId, logId,
baseLogId, baseLogId,
createFilterId, createFilterId,
allCase, cases,
postRuleData, postRuleData,
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
@@ -401,7 +401,7 @@ function setNodesData(mapData) {
}; };
mapData.nodes = []; mapData.nodes = [];
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value; const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.vertices.forEach((node) => { mapSource.vertices.forEach((node) => {
switch (node.type) { switch (node.type) {
case "gateway": case "gateway":
@@ -485,7 +485,7 @@ function setEdgesData(mapData) {
}; };
mapData.edges = []; mapData.edges = [];
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value; const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.edges.forEach((edge) => { mapSource.edges.forEach((edge) => {
mapData.edges.push({ mapData.edges.push({
data: { data: {
@@ -510,7 +510,7 @@ function setEdgesData(mapData) {
async function createCy(type) { async function createCy(type) {
const graphId = document.getElementById("cy"); const graphId = document.getElementById("cy");
const mapData = type === "processMap" ? processMapData.value : bpmnData.value; const mapData = type === "processMap" ? processMapData.value : bpmnData.value;
const mapSource = type === "processMap" ? processMap.value : allBpmn.value; const mapSource = type === "processMap" ? processMap.value : bpmn.value;
if (mapSource.vertices.length !== 0) { if (mapSource.vertices.length !== 0) {
setNodesData(mapData); setNodesData(mapData);
@@ -42,7 +42,7 @@ defineOptions({
break; break;
} }
await conformanceStore.getConformanceReport(); await conformanceStore.getConformanceReport();
to.meta.file = await conformanceStore.allConformanceTempReportData?.file; to.meta.file = await conformanceStore.conformanceTempReportData?.file;
} }
next(); next();
}, },
@@ -68,7 +68,7 @@ const {
selectedActSeqMore, selectedActSeqMore,
selectedActSeqFromTo, selectedActSeqFromTo,
conformanceRuleData, conformanceRuleData,
allConformanceTempReportData, conformanceTempReportData,
conformanceFileName, conformanceFileName,
} = storeToRefs(conformanceStore); } = storeToRefs(conformanceStore);
+21 -21
View File
@@ -86,22 +86,22 @@
></SidebarView> ></SidebarView>
<SidebarState <SidebarState
v-model:visible="sidebarState" v-model:visible="sidebarState"
:insights="allInsights" :insights="insights"
:stats="allStats" :stats="stats"
></SidebarState> ></SidebarState>
<SidebarTraces <SidebarTraces
v-model:visible="sidebarTraces" v-model:visible="sidebarTraces"
:cases="allCase" :cases="cases"
@switch-Trace-Id="switchTraceId" @switch-Trace-Id="switchTraceId"
ref="tracesViewRef" ref="tracesViewRef"
></SidebarTraces> ></SidebarTraces>
<SidebarFilter <SidebarFilter
v-model:visible="sidebarFilter" v-model:visible="sidebarFilter"
:filterTasks="allFilterTask" :filterTasks="filterTasks"
:filterStartToEnd="allFilterStartToEnd" :filterStartToEnd="filterStartToEnd"
:filterEndToStart="allFilterEndToStart" :filterEndToStart="filterEndToStart"
:filterTimeframe="allFilterTimeframe" :filterTimeframe="filterTimeframe"
:filterTrace="allFilterTrace" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @submit-all="createCy(mapType)"
@switch-Trace-Id="switchTraceId" @switch-Trace-Id="switchTraceId"
ref="sidebarFilterRefComp" ref="sidebarFilterRefComp"
@@ -137,7 +137,7 @@ export default {
break; break;
} }
await conformanceStore.getConformanceReport(true); await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.allRouteFile; to.meta.file = conformanceStore.routeFile;
} }
next(); next();
}, },
@@ -175,25 +175,25 @@ const allMapDataStore = useAllMapDataStore();
const { isLoading } = storeToRefs(loadingStore); const { isLoading } = storeToRefs(loadingStore);
const { const {
processMap, processMap,
allBpmn, bpmn,
allStats, stats,
allInsights, insights,
traceId, traceId,
traces, traces,
baseTraces, baseTraces,
baseTraceId, baseTraceId,
allFilterTask, filterTasks,
allFilterStartToEnd, filterStartToEnd,
allFilterEndToStart, filterEndToStart,
allFilterTimeframe, filterTimeframe,
allFilterTrace, filterTrace,
temporaryData, temporaryData,
isRuleData, isRuleData,
ruleData, ruleData,
logId, logId,
baseLogId, baseLogId,
createFilterId, createFilterId,
allCase, cases,
postRuleData, postRuleData,
} = storeToRefs(allMapDataStore); } = storeToRefs(allMapDataStore);
@@ -398,7 +398,7 @@ function setNodesData(mapData) {
}; };
mapData.nodes = []; mapData.nodes = [];
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value; const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.vertices.forEach((node) => { mapSource.vertices.forEach((node) => {
switch (node.type) { switch (node.type) {
case "gateway": case "gateway":
@@ -477,7 +477,7 @@ function setEdgesData(mapData) {
}; };
mapData.edges = []; mapData.edges = [];
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value; const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
mapSource.edges.forEach((edge) => { mapSource.edges.forEach((edge) => {
mapData.edges.push({ mapData.edges.push({
data: { data: {
@@ -499,7 +499,7 @@ function setEdgesData(mapData) {
async function createCy(type) { async function createCy(type) {
const graphId = document.getElementById("cy"); const graphId = document.getElementById("cy");
const mapData = type === "processMap" ? processMapData.value : bpmnData.value; const mapData = type === "processMap" ? processMapData.value : bpmnData.value;
const mapSource = type === "processMap" ? processMap.value : allBpmn.value; const mapSource = type === "processMap" ? processMap.value : bpmn.value;
if (mapSource.vertices.length !== 0) { if (mapSource.vertices.length !== 0) {
setNodesData(mapData); setNodesData(mapData);
@@ -263,7 +263,7 @@ export default {
break; break;
} }
await conformanceStore.getConformanceReport(true); await conformanceStore.getConformanceReport(true);
to.meta.file = conformanceStore.allRouteFile; to.meta.file = conformanceStore.routeFile;
} }
next(); next();
}, },
-1
View File
@@ -8,7 +8,6 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({ export default defineConfig({
testDir: "./specs", testDir: "./specs",
timeout: 30000, timeout: 30000,
workers: 4,
expect: { timeout: 5000 }, expect: { timeout: 5000 },
use: { use: {
baseURL: "http://localhost:4173", baseURL: "http://localhost:4173",
@@ -1,63 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/07/04
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
// Mock localStorage since jsdom's localStorage is limited
const localStorageMock = (() => {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => {
store[key] = value;
}),
removeItem: vi.fn((key) => {
delete store[key];
}),
clear: vi.fn(() => {
store = {};
}),
};
})();
Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
});
describe("cytoscapeStore saveNodePosition without stored data", () => {
let store;
beforeEach(() => {
setActivePinia(createPinia());
store = useCytoscapeStore();
localStorageMock.clear();
vi.clearAllMocks();
// Keep localStorage empty for SAVE_KEY_NAME throughout, as when
// storage is unavailable or cleared externally between saves.
localStorageMock.getItem.mockReturnValue(null);
});
it("does not duplicate a node saved twice without stored data", () => {
store.setCurrentGraphId("graph1");
store.saveNodePosition("node1", { x: 10, y: 20 }, "TB");
store.saveNodePosition("node1", { x: 30, y: 40 }, "TB");
expect(store.nodePositions["graph1"]["TB"]).toHaveLength(1);
expect(store.nodePositions["graph1"]["TB"][0]).toEqual({
id: "node1",
position: { x: 30, y: 40 },
});
});
it("adds separate entries for different nodes", () => {
store.setCurrentGraphId("graph1");
store.saveNodePosition("node1", { x: 10, y: 20 }, "TB");
store.saveNodePosition("node2", { x: 30, y: 40 }, "TB");
expect(store.nodePositions["graph1"]["TB"]).toEqual([
{ id: "node1", position: { x: 10, y: 20 } },
{ id: "node2", position: { x: 30, y: 40 } },
]);
});
});
-353
View File
@@ -1,353 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/7/5
// AI assistance: Claude Code (Anthropic)
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { http, HttpResponse } from "msw";
import { server } from "@/mocks/node.js";
import { findRequest, captureRequest } from "@/mocks/request-log.js";
vi.mock("@/module/apiError.js", () => ({
default: vi.fn(),
}));
vi.mock("sweetalert2", () => ({
default: { close: vi.fn(), fire: vi.fn() },
}));
import Swal from "sweetalert2";
import {
saveFilter,
saveConformance,
leaveFilter,
leaveConformance,
} from "@/module/alertModal.js";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { usePageAdminStore } from "@/stores/pageAdmin";
/**
* Makes Swal.fire simulate a confirmed dialog. If the dialog config
* has an input validator, it is invoked with the value first, the
* same way SweetAlert2 validates the input before resolving.
*
* @param {string} value - The input value the user typed.
*/
function fireConfirmedWithInput(value) {
Swal.fire.mockImplementation(async (config) => {
if (config.inputValidator) config.inputValidator(value);
return { value, isConfirmed: true };
});
}
describe("alertModal", () => {
let pageAdminStore;
beforeEach(() => {
setActivePinia(createPinia());
pageAdminStore = usePageAdminStore();
vi.spyOn(pageAdminStore, "keepPreviousPage");
Swal.fire.mockReset();
document.cookie = "luciaToken=fake-test-token";
});
describe("saveFilter", () => {
it("prompts with the filter save dialog config", async () => {
fireConfirmedWithInput("My Filter");
await saveFilter(vi.fn());
expect(Swal.fire).toHaveBeenCalledWith(
expect.objectContaining({
title: "SAVE NEW FILTER",
input: "text",
inputPlaceholder: "Enter Filter Name.",
icon: "info",
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: "#94a3b8",
}),
);
});
it("rejects an empty input value", async () => {
fireConfirmedWithInput("My Filter");
await saveFilter(vi.fn());
const config = Swal.fire.mock.calls[0][0];
expect(config.inputValidator("")).toBe("You need to write something!");
expect(config.inputValidator("ok")).toBeUndefined();
});
it("saves via the API function and notifies on confirm", async () => {
fireConfirmedWithInput("My Filter");
const addFilterId = vi.fn();
const saved = await saveFilter(addFilterId);
expect(saved).toBe(true);
expect(addFilterId).toHaveBeenCalledWith("My Filter");
expect(Swal.fire).toHaveBeenCalledTimes(2);
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE COMPLETE",
icon: "success",
});
expect(Swal.fire.mock.calls[1][0].html).toContain("My Filter");
expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled();
});
it("keeps the previous page and stops navigation on cancel", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const addFilterId = vi.fn();
const next = vi.fn();
const saved = await saveFilter(addFilterId, next);
expect(saved).toBe(false);
expect(addFilterId).not.toHaveBeenCalled();
expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith(false);
});
it("cancels without a next() callback", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const saved = await saveFilter(vi.fn());
expect(saved).toBe(false);
expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1);
});
});
describe("saveConformance", () => {
it("prompts with the rule save dialog config", async () => {
fireConfirmedWithInput("My Rule");
await saveConformance(vi.fn());
expect(Swal.fire).toHaveBeenCalledWith(
expect.objectContaining({
title: "SAVE NEW RULE",
input: "text",
inputPlaceholder: "Enter Rule Name.",
icon: "info",
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: "#94a3b8",
}),
);
});
it("saves via the API function and notifies on confirm", async () => {
fireConfirmedWithInput("My Rule");
const addCheckId = vi.fn();
const saved = await saveConformance(addCheckId);
expect(saved).toBe(true);
expect(addCheckId).toHaveBeenCalledWith("My Rule");
expect(Swal.fire).toHaveBeenCalledTimes(2);
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE COMPLETE",
icon: "success",
});
expect(Swal.fire.mock.calls[1][0].html).toContain("My Rule");
});
it("returns false on cancel without touching the page admin", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const addCheckId = vi.fn();
const saved = await saveConformance(addCheckId);
expect(saved).toBe(false);
expect(addCheckId).not.toHaveBeenCalled();
expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled();
});
});
describe("leaveFilter", () => {
let allMapDataStore;
beforeEach(() => {
allMapDataStore = useAllMapDataStore();
});
it("shows the leave confirmation dialog config", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "esc" });
await leaveFilter(vi.fn(), vi.fn(), "/files");
expect(Swal.fire).toHaveBeenCalledWith(
expect.objectContaining({
title: "SAVE YOUR FILTER?",
html: "If you want to continue using this filter in any other page, please select [Yes].",
icon: "warning",
confirmButtonText: "Yes",
cancelButtonText: "No",
confirmButtonColor: "#FF3366",
showCancelButton: true,
}),
);
});
it("updates an existing filter and navigates on confirm", async () => {
server.use(
http.put("/api/filters/:id", ({ params }) => {
captureRequest("PUT", `/api/filters/${params.id}`);
return HttpResponse.json({});
}),
);
allMapDataStore.createFilterId = 5;
allMapDataStore.filterName = "My Filter";
Swal.fire.mockResolvedValue({ isConfirmed: true });
const next = vi.fn();
await leaveFilter(next, vi.fn(), "/files");
expect(findRequest("PUT", "/api/filters/5")).toHaveLength(1);
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE COMPLETE",
});
expect(Swal.fire.mock.calls[1][0].html).toContain("My Filter");
expect(next).toHaveBeenCalledWith("/files");
});
it("prompts to save a new filter and navigates on confirm", async () => {
Swal.fire.mockImplementationOnce(async () => ({ isConfirmed: true }));
Swal.fire.mockImplementationOnce(async (config) => {
config.inputValidator("Fresh Filter");
return { value: "Fresh Filter", isConfirmed: true };
});
const addFilterId = vi.fn();
const next = vi.fn();
await leaveFilter(next, addFilterId, "/files");
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE NEW FILTER",
});
expect(addFilterId).toHaveBeenCalledWith("Fresh Filter");
expect(next).toHaveBeenCalledWith("/files");
});
it("calls logOut instead of navigating on confirm", async () => {
server.use(http.put("/api/filters/:id", () => HttpResponse.json({})));
allMapDataStore.createFilterId = 5;
allMapDataStore.filterName = "My Filter";
Swal.fire.mockResolvedValue({ isConfirmed: true });
const next = vi.fn();
const logOut = vi.fn();
await leaveFilter(next, vi.fn(), "/files", logOut);
expect(logOut).toHaveBeenCalledTimes(1);
expect(next).not.toHaveBeenCalled();
});
it("discards the filter and navigates on cancel", async () => {
allMapDataStore.tempFilterId = 42;
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const next = vi.fn();
await leaveFilter(next, vi.fn(), "/files");
expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1);
expect(allMapDataStore.tempFilterId).toBeNull();
expect(next).toHaveBeenCalledWith("/files");
});
it("stays on the page on backdrop dismiss", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" });
const next = vi.fn();
await leaveFilter(next, vi.fn(), "/files");
expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith(false);
});
it("does nothing extra on backdrop dismiss when logging out", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" });
const next = vi.fn();
const logOut = vi.fn();
await leaveFilter(next, vi.fn(), "/files", logOut);
expect(pageAdminStore.keepPreviousPage).toHaveBeenCalledTimes(1);
expect(next).not.toHaveBeenCalled();
expect(logOut).not.toHaveBeenCalled();
});
});
describe("leaveConformance", () => {
let conformanceStore;
beforeEach(() => {
conformanceStore = useConformanceStore();
});
it("shows the leave confirmation dialog config", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "esc" });
await leaveConformance(vi.fn(), vi.fn(), "/files");
expect(Swal.fire).toHaveBeenCalledWith(
expect.objectContaining({
title: "SAVE YOUR RULE?",
icon: "warning",
confirmButtonText: "Yes",
cancelButtonText: "No",
confirmButtonColor: "#FF3366",
showCancelButton: true,
}),
);
expect(Swal.fire.mock.calls[0][0].html).toBeUndefined();
});
it("updates an existing rule without navigating on confirm", async () => {
server.use(
http.put("/api/filter-checks/:id", ({ params }) => {
captureRequest("PUT", `/api/filter-checks/${params.id}`);
return HttpResponse.json({});
}),
);
conformanceStore.conformanceFilterCreateCheckId = 7;
conformanceStore.conformanceFileName = "My Rule";
Swal.fire.mockResolvedValue({ isConfirmed: true });
const next = vi.fn();
await leaveConformance(next, vi.fn(), "/files");
expect(findRequest("PUT", "/api/filter-checks/7")).toHaveLength(1);
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE COMPLETE",
});
expect(Swal.fire.mock.calls[1][0].html).toContain("My Rule");
expect(next).not.toHaveBeenCalled();
});
it("prompts to save a new rule without navigating on confirm", async () => {
Swal.fire.mockImplementationOnce(async () => ({ isConfirmed: true }));
Swal.fire.mockImplementationOnce(async (config) => {
config.inputValidator("Fresh Rule");
return { value: "Fresh Rule", isConfirmed: true };
});
const addCheckId = vi.fn();
const next = vi.fn();
await leaveConformance(next, addCheckId, "/files");
expect(Swal.fire.mock.calls[1][0]).toMatchObject({
title: "SAVE NEW RULE",
});
expect(addCheckId).toHaveBeenCalledWith("Fresh Rule");
expect(next).not.toHaveBeenCalled();
});
it("resets temporary check IDs and navigates on cancel", async () => {
conformanceStore.conformanceFilterTempCheckId = 3;
conformanceStore.conformanceLogTempCheckId = 4;
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const next = vi.fn();
await leaveConformance(next, vi.fn(), "/files");
expect(conformanceStore.conformanceFilterTempCheckId).toBeNull();
expect(conformanceStore.conformanceLogTempCheckId).toBeNull();
expect(next).toHaveBeenCalledWith("/files");
expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled();
});
it("calls logOut instead of navigating on cancel", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "cancel" });
const next = vi.fn();
const logOut = vi.fn();
await leaveConformance(next, vi.fn(), "/files", logOut);
expect(logOut).toHaveBeenCalledTimes(1);
expect(next).not.toHaveBeenCalled();
});
it("stays on the page on backdrop dismiss", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" });
const next = vi.fn();
await leaveConformance(next, vi.fn(), "/files");
expect(next).toHaveBeenCalledWith(false);
expect(pageAdminStore.keepPreviousPage).not.toHaveBeenCalled();
});
it("does nothing on backdrop dismiss when logging out", async () => {
Swal.fire.mockResolvedValue({ isConfirmed: false, dismiss: "backdrop" });
const next = vi.fn();
const logOut = vi.fn();
await leaveConformance(next, vi.fn(), "/files", logOut);
expect(next).not.toHaveBeenCalled();
expect(logOut).not.toHaveBeenCalled();
});
});
});
-23
View File
@@ -1,23 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/07/04
// AI assistance: Claude Code (Anthropic)
import { describe, it, expect, vi } from "vitest";
import apiError from "@/module/apiError.js";
const toastDefault = vi.fn();
vi.mock("vue-toast-notification", () => ({
useToast: () => ({ default: toastDefault }),
}));
describe("apiError", () => {
it("shows the message as a toast at the bottom", () => {
apiError("some message");
expect(toastDefault).toHaveBeenCalledWith("some message", {
position: "bottom",
});
});
});
-61
View File
@@ -1,61 +0,0 @@
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/7/5
// AI assistance: Claude Code (Anthropic)
import { describe, it, expect } from "vitest";
import formatDuration, { pickDurationUnit } from "@/module/formatDuration.js";
describe("formatDuration", () => {
it("formats a full cascade with the default separator", () => {
expect(formatDuration(93784)).toBe("1d 2h 3m 4s");
});
it("omits zero-valued units", () => {
expect(formatDuration(86400)).toBe("1d");
expect(formatDuration(3661)).toBe("1h 1m 1s");
});
it("returns an empty string when no unit qualifies", () => {
expect(formatDuration(0)).toBe("");
expect(formatDuration(NaN)).toBe("");
});
it("supports a custom separator", () => {
expect(formatDuration(90061, { separator: "" })).toBe("1d1h1m1s");
});
it("always appends seconds when alwaysSeconds is set", () => {
expect(formatDuration(0, { alwaysSeconds: true })).toBe("0s");
expect(formatDuration(60, { separator: "", alwaysSeconds: true })).toBe(
"1m0s",
);
});
it("truncates to maxUnits largest units", () => {
expect(formatDuration(93784, { maxUnits: 2 })).toBe("1d 2h");
expect(formatDuration(45, { maxUnits: 2 })).toBe("45s");
});
});
describe("pickDurationUnit", () => {
it("picks the largest unit reached by the value", () => {
expect(pickDurationUnit(86400).symbol).toBe("d");
expect(pickDurationUnit(3600).symbol).toBe("h");
expect(pickDurationUnit(60).symbol).toBe("m");
expect(pickDurationUnit(59).symbol).toBe("s");
});
it("requires strictly more than one unit when exclusive", () => {
expect(pickDurationUnit(86400, true).symbol).toBe("h");
expect(pickDurationUnit(86401, true).symbol).toBe("d");
expect(pickDurationUnit(60, true).symbol).toBe("s");
expect(pickDurationUnit(61, true).symbol).toBe("m");
});
it("falls back to seconds for zero and negative values", () => {
expect(pickDurationUnit(0).symbol).toBe("s");
expect(pickDurationUnit(-100).symbol).toBe("s");
});
});
-136
View File
@@ -1,136 +0,0 @@
#!/usr/bin/env node
// The Lucia project.
// Copyright 2026-2026 DSP, inc. All rights reserved.
// Authors:
// imacat.yang@dsp.im (imacat), 2026/03/22
/**
* Validates MSW fixture JSON files against the OpenAPI spec.
* Ensures mock data matches the real API contract.
*
* Usage: node tests/validate-fixtures.js
*/
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "..");
const spec = JSON.parse(
readFileSync(resolve(root, "excludes/openapi.json"), "utf-8"),
);
const schemas = spec.components?.schemas ?? {};
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
// Add ISO 8601 duration format (not included in ajv-formats)
ajv.addFormat("duration", {
type: "string",
validate: (s) => /^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(s),
});
// Register all schemas from the OpenAPI spec
for (const [name, schema] of Object.entries(schemas)) {
ajv.addSchema(schema, `#/components/schemas/${name}`);
}
/**
* Resolves a JSON Schema $ref to the actual schema object.
* @param {object} schema - Schema that may contain $ref.
* @returns {object} Resolved schema.
*/
function resolveRef(schema) {
if (schema?.$ref) {
const refName = schema.$ref.replace("#/components/schemas/", "");
return schemas[refName] ?? schema;
}
return schema;
}
/**
* Gets the response schema for a given path and method.
* @param {string} method - HTTP method (lowercase).
* @param {string} path - API path.
* @returns {object|null} Resolved schema or null.
*/
function getResponseSchema(method, path) {
const pathInfo = spec.paths?.[path];
if (!pathInfo) return null;
const methodInfo = pathInfo[method];
if (!methodInfo) return null;
const responses = methodInfo.responses ?? {};
const resp = responses["200"] ?? responses["201"] ?? {};
const content = resp.content?.["application/json"] ?? {};
return content.schema ? resolveRef(content.schema) : null;
}
// Fixture-to-endpoint mapping
const fixtures = [
["token.json", "post", "/oauth/token"],
["my-account.json", "get", "/my-account"],
["users.json", "get", "/users"],
["user-detail.json", "get", "/users/{username}"],
["files.json", "get", "/files"],
["discover.json", "get", "/logs/{log_id}/discover"],
["performance.json", "get", "/logs/{log_id}/performance"],
["traces.json", "get", "/logs/{log_id}/traces"],
["trace-detail.json", "get", "/logs/{log_id}/traces/{trace_id}"],
// compare.json: real API returns numbers for duration y-values and
// dates without Z suffix, which doesn't match the OpenAPI spec.
// Skipped until the spec is updated to match the actual API.
// ["compare.json", "get", "/compare"],
["filter-params.json", "get", "/filters/params"],
];
let passed = 0;
let failed = 0;
for (const [fixture, method, path] of fixtures) {
const data = JSON.parse(
readFileSync(
resolve(root, "src/mocks/fixtures", fixture),
"utf-8",
),
);
const schema = getResponseSchema(method, path);
if (!schema) {
console.log(`${fixture}: no schema found for ${method.toUpperCase()} ${path}`);
continue;
}
// For array responses, validate the schema's items against each element
if (schema.type === "array" && schema.items) {
const itemSchema = resolveRef(schema.items);
let allValid = true;
for (let i = 0; i < data.length; i++) {
const valid = ajv.validate(itemSchema, data[i]);
if (!valid) {
console.log(`${fixture}[${i}]: ${ajv.errorsText()}`);
allValid = false;
}
}
if (allValid) {
console.log(`${fixture} (${data.length} items)`);
passed++;
} else {
failed++;
}
} else {
const valid = ajv.validate(schema, data);
if (valid) {
console.log(`${fixture}`);
passed++;
} else {
console.log(`${fixture}: ${ajv.errorsText()}`);
failed++;
}
}
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
+8
View File
@@ -46,7 +46,9 @@ export default defineConfig(({ mode }) => {
base: "/", base: "/",
resolve: { resolve: {
alias: { alias: {
util: "util/",
"@": fileURLToPath(new URL("./src", import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),
"/images": "src/assets/images",
}, },
}, },
server: { server: {
@@ -59,7 +61,13 @@ export default defineConfig(({ mode }) => {
port: 4173, port: 4173,
proxy, proxy,
}, },
optimizeDeps: {
include: ["vue", "vue-router", "pinia", "axios"],
},
build: { build: {
commonjsOptions: {
transformMixedEsModules: true, // Enable @walletconnect/web3-provider which has some code in CommonJS
},
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {