Compare commits
2
Commits
main
..
f828bd0423
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f828bd0423 | ||
|
|
6e7d010c54 |
@@ -28,7 +28,7 @@ and [Tailwind CSS][tailwind].
|
||||
| UI | PrimeVue 4, PrimeIcons, SweetAlert2 |
|
||||
| Visualization | Cytoscape.js (process maps), Chart.js (charts) |
|
||||
| HTTP | Axios with JWT refresh token handling |
|
||||
| Testing | Vitest 4 (unit/component), [Playwright][playwright] (E2E), [MSW][msw] (API mocking) |
|
||||
| Testing | Vitest 4 (unit/component), Cypress 15 (E2E) |
|
||||
| Linting | ESLint, Prettier |
|
||||
|
||||
|
||||
@@ -80,18 +80,18 @@ npx vitest run
|
||||
|
||||
### Run E2E tests
|
||||
|
||||
Build with MSW enabled first, then run [Playwright][playwright]:
|
||||
Build first, then run [Cypress][cypress] against the preview
|
||||
server:
|
||||
|
||||
```sh
|
||||
npm run build:e2e
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
For interactive E2E development with the Playwright UI:
|
||||
For interactive E2E development with the Vite dev server:
|
||||
|
||||
```sh
|
||||
npm run build:e2e
|
||||
npm run test:e2e:ui
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
|
||||
@@ -169,7 +169,6 @@ Code quality improvements assisted by [Claude Code][claude-code].
|
||||
[cytoscape]: https://js.cytoscape.org/
|
||||
[chartjs]: https://www.chartjs.org/
|
||||
[nodejs]: https://nodejs.org/
|
||||
[playwright]: https://playwright.dev/
|
||||
[msw]: https://mswjs.io/
|
||||
[cypress]: https://www.cypress.io/
|
||||
[typedoc]: https://typedoc.org/
|
||||
[claude-code]: https://claude.ai/claude-code
|
||||
|
||||
Generated
+152
-116
@@ -27,13 +27,14 @@
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.5.4",
|
||||
"sweetalert2": "^11.4.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vue": "^3.5.29",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue-sweetalert2": "^5.0.11",
|
||||
"vue-toast-notification": "^3.1.3",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
@@ -47,8 +48,7 @@
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chartjs-plugin-dragdata": "^2.3.1",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"jsdom": "^28.1.0",
|
||||
@@ -2964,40 +2964,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"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.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
"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/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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -3199,6 +3181,44 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
@@ -3540,6 +3560,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -3970,23 +3996,6 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
@@ -4000,13 +4009,6 @@
|
||||
"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": {
|
||||
"version": "11.1.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz",
|
||||
@@ -4134,23 +4136,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4786,9 +4771,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"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"
|
||||
},
|
||||
@@ -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": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
@@ -6171,7 +6181,6 @@
|
||||
"version": "11.4.4",
|
||||
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.4.tgz",
|
||||
"integrity": "sha512-9yYWQuRT1v9JNI/paPTSYV+68MHwe9C+HQ/I2jtfaFzoJgYRftWXOs4JqmDSjT7m2m4r8ebMMn8LcxD1Wq9B/w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz",
|
||||
@@ -8796,24 +8816,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"ajv": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"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.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.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"
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
@@ -8961,6 +8972,34 @@
|
||||
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
"defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -9425,29 +9469,11 @@
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -10015,9 +10035,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"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
|
||||
},
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
@@ -11251,6 +11279,14 @@
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz",
|
||||
|
||||
+3
-3
@@ -36,13 +36,14 @@
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.5.4",
|
||||
"sweetalert2": "^11.4.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"vue": "^3.5.29",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue-sweetalert2": "^5.0.11",
|
||||
"vue-toast-notification": "^3.1.3",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
@@ -56,8 +57,7 @@
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chartjs-plugin-dragdata": "^2.3.1",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"jsdom": "^28.1.0",
|
||||
|
||||
@@ -358,8 +358,8 @@
|
||||
<MoreModal
|
||||
:listModal="issuesModal"
|
||||
@closeModal="issuesModal = $event"
|
||||
:listTraces="allIssueTraces"
|
||||
:taskSeq="allTaskSeq"
|
||||
:listTraces="issueTraces"
|
||||
:taskSeq="taskSeq"
|
||||
:cases="cases"
|
||||
:listNo="issuesNo"
|
||||
:traceId="traceId"
|
||||
@@ -369,8 +369,8 @@
|
||||
<MoreModal
|
||||
:listModal="loopModal"
|
||||
@closeModal="loopModal = $event"
|
||||
:listTraces="allLoopTraces"
|
||||
:taskSeq="allLoopTaskSeq"
|
||||
:listTraces="loopTraces"
|
||||
:taskSeq="loopTaskSeq"
|
||||
:cases="loopCases"
|
||||
:listNo="loopNo"
|
||||
:traceId="looptraceId"
|
||||
@@ -413,12 +413,12 @@ import emitter from "@/utils/emitter";
|
||||
|
||||
const conformanceStore = useConformanceStore();
|
||||
const {
|
||||
allConformanceTempReportData,
|
||||
allIssueTraces,
|
||||
allTaskSeq,
|
||||
conformanceTempReportData,
|
||||
issueTraces,
|
||||
taskSeq,
|
||||
cases,
|
||||
allLoopTraces,
|
||||
allLoopTaskSeq,
|
||||
loopTraces,
|
||||
loopTaskSeq,
|
||||
loopCases,
|
||||
} = storeToRefs(conformanceStore);
|
||||
|
||||
@@ -525,11 +525,11 @@ const openMore = async (no) => {
|
||||
// Use async/await to prevent errors caused by asynchronous data not being available yet
|
||||
issuesNo.value = no;
|
||||
await conformanceStore.getConformanceIssue(no);
|
||||
if (allIssueTraces.value.length === 0) return;
|
||||
traceId.value = allIssueTraces.value[0].id;
|
||||
if (issueTraces.value.length === 0) return;
|
||||
traceId.value = issueTraces.value[0].id;
|
||||
firstCases.value = await conformanceStore.getConformanceTraceDetail(
|
||||
no,
|
||||
allIssueTraces.value[0].id,
|
||||
issueTraces.value[0].id,
|
||||
0,
|
||||
);
|
||||
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
|
||||
loopNo.value = no;
|
||||
await conformanceStore.getConformanceLoop(no);
|
||||
if (allLoopTraces.value.length === 0) return;
|
||||
looptraceId.value = allLoopTraces.value[0].id;
|
||||
if (loopTraces.value.length === 0) return;
|
||||
looptraceId.value = loopTraces.value[0].id;
|
||||
loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail(
|
||||
no,
|
||||
allLoopTraces.value[0].id,
|
||||
loopTraces.value[0].id,
|
||||
0,
|
||||
);
|
||||
loopModal.value = true;
|
||||
@@ -959,7 +959,7 @@ const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
|
||||
};
|
||||
|
||||
// watch
|
||||
watch(allConformanceTempReportData, (newValue) => {
|
||||
watch(conformanceTempReportData, (newValue) => {
|
||||
if (newValue?.rule && newValue.rule.min !== null) {
|
||||
selectDurationTime.value = {
|
||||
min: newValue.rule.min,
|
||||
|
||||
@@ -180,7 +180,7 @@ const {
|
||||
selectedProcessScope,
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
allConformanceTask,
|
||||
conformanceAllTasks,
|
||||
cfmPtEteWhole,
|
||||
cfmPtEteStart: cfmPtEteStartStore,
|
||||
cfmPtEteEnd: cfmPtEteEndStore,
|
||||
@@ -204,7 +204,7 @@ const {
|
||||
conformanceRuleData,
|
||||
conformanceLogCreateCheckId,
|
||||
conformanceFilterCreateCheckId,
|
||||
allConformanceTempReportData,
|
||||
conformanceTempReportData,
|
||||
} = storeToRefs(conformanceStore);
|
||||
const { activityRadioData } = storeToRefs(conformanceInputStore);
|
||||
|
||||
@@ -391,7 +391,7 @@ const isApplyBtnDisabled = computed(() => {
|
||||
|
||||
// When opening a rule file, display the saved options, rules, and time
|
||||
let reportWatchTimerId = null;
|
||||
watch(allConformanceTempReportData, (newValue) => {
|
||||
watch(conformanceTempReportData, (newValue) => {
|
||||
if (reportWatchTimerId) clearTimeout(reportWatchTimerId);
|
||||
reportWatchTimerId = setTimeout(() => {
|
||||
if (newValue !== null) {
|
||||
|
||||
@@ -376,8 +376,8 @@ const {
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceTask,
|
||||
allCfmSeqStart,
|
||||
allCfmSeqEnd,
|
||||
cfmSeqStart,
|
||||
cfmSeqEnd,
|
||||
cfmPtEteStart,
|
||||
cfmPtEteEnd,
|
||||
cfmPtEteSE,
|
||||
@@ -435,14 +435,14 @@ const taskEnd = ref(null);
|
||||
const cfmSeqStartData = computed(() => {
|
||||
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
|
||||
return isEndSelected.value
|
||||
? setSeqStartAndEndData(allCfmSeqEnd.value, "sources", selectedTask)
|
||||
: allCfmSeqStart.value.map((i) => i.label);
|
||||
? setSeqStartAndEndData(cfmSeqEnd.value, "sources", selectedTask)
|
||||
: cfmSeqStart.value.map((i) => i.label);
|
||||
});
|
||||
const cfmSeqEndData = computed(() => {
|
||||
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
|
||||
return isStartSelected.value
|
||||
? setSeqStartAndEndData(allCfmSeqStart.value, "sinks", selectedTask)
|
||||
: allCfmSeqEnd.value.map((i) => i.label);
|
||||
? setSeqStartAndEndData(cfmSeqStart.value, "sinks", selectedTask)
|
||||
: cfmSeqEnd.value.map((i) => i.label);
|
||||
});
|
||||
// Processing time
|
||||
const cfmPtEteStartData = computed(() => {
|
||||
@@ -584,7 +584,7 @@ watch(
|
||||
|
||||
/**
|
||||
* 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'.
|
||||
* @returns {array}
|
||||
*/
|
||||
@@ -615,7 +615,7 @@ function setStartAndEndData(data, category, taskVal) {
|
||||
}
|
||||
/**
|
||||
* 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} task - The selected activity task.
|
||||
* @returns {array}
|
||||
|
||||
@@ -250,10 +250,10 @@ const {
|
||||
selectedProcessScope,
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
allConformanceTask,
|
||||
conformanceAllTasks,
|
||||
conformanceTask,
|
||||
allCfmSeqStart,
|
||||
allCfmSeqEnd,
|
||||
cfmSeqStart,
|
||||
cfmSeqEnd,
|
||||
cfmPtEteStart,
|
||||
cfmPtEteEnd,
|
||||
cfmPtEteSE,
|
||||
@@ -471,7 +471,7 @@ emitter.on("actRadioData", (data) => {
|
||||
// Activity duration
|
||||
case "cfmDur":
|
||||
state.timeDuration = getDurationTime(
|
||||
allConformanceTask.value,
|
||||
conformanceAllTasks.value,
|
||||
"act",
|
||||
task,
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ import getMoment from "moment";
|
||||
const props = defineProps(["selectValue"]);
|
||||
|
||||
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 selectArea = ref(null);
|
||||
@@ -120,8 +120,8 @@ watch(timeFrameStartEnd, (newValue) => {
|
||||
|
||||
// Compute slider data; time format: millisecond timestamps
|
||||
const sliderData = computed(() => {
|
||||
const xAxisMin = new Date(allFilterTimeframe.value.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(allFilterTimeframe.value.x_axis.max).getTime();
|
||||
const xAxisMin = new Date(filterTimeframe.value.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
|
||||
const range = xAxisMax - xAxisMin;
|
||||
const step = range / selectRange.value;
|
||||
const data = [];
|
||||
@@ -135,9 +135,9 @@ const sliderData = computed(() => {
|
||||
|
||||
// Add the minimum and maximum values
|
||||
const timeFrameData = computed(() => {
|
||||
if (!allFilterTimeframe.value?.data || allFilterTimeframe.value.data.length < 10)
|
||||
if (!filterTimeframe.value?.data || filterTimeframe.value.data.length < 10)
|
||||
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
|
||||
// x values are 0 ~ 11,
|
||||
// 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;
|
||||
let b;
|
||||
const c = 1;
|
||||
const d = allFilterTimeframe.value.data[0].y;
|
||||
const d = filterTimeframe.value.data[0].y;
|
||||
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);
|
||||
if (b < 0) {
|
||||
b = 0;
|
||||
}
|
||||
// Y-axis maximum value
|
||||
const ma = 9;
|
||||
const mb = allFilterTimeframe.value.data[8].y;
|
||||
const mb = filterTimeframe.value.data[8].y;
|
||||
const mc = 10;
|
||||
const md = allFilterTimeframe.value.data[9].y;
|
||||
const md = filterTimeframe.value.data[9].y;
|
||||
const me = 11;
|
||||
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
|
||||
if (mf < 0) {
|
||||
@@ -168,12 +168,12 @@ const timeFrameData = computed(() => {
|
||||
|
||||
// Add the minimum value
|
||||
data.unshift({
|
||||
x: allFilterTimeframe.value.x_axis.min_base,
|
||||
x: filterTimeframe.value.x_axis.min_base,
|
||||
y: b,
|
||||
});
|
||||
// Add the maximum value
|
||||
data.push({
|
||||
x: allFilterTimeframe.value.x_axis.max_base,
|
||||
x: filterTimeframe.value.x_axis.max_base,
|
||||
y: mf,
|
||||
});
|
||||
|
||||
@@ -181,8 +181,8 @@ const timeFrameData = computed(() => {
|
||||
});
|
||||
|
||||
const labelsData = computed(() => {
|
||||
const min = new Date(allFilterTimeframe.value.x_axis.min_base).getTime();
|
||||
const max = new Date(allFilterTimeframe.value.x_axis.max_base).getTime();
|
||||
const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
|
||||
const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
|
||||
const numPoints = 11;
|
||||
const step = (max - min) / (numPoints - 1);
|
||||
const data = [];
|
||||
@@ -195,8 +195,8 @@ const labelsData = computed(() => {
|
||||
|
||||
watch(selectTimeFrame, (newValue, oldValue) => {
|
||||
if (newValue.length === 0) {
|
||||
startTime.value = new Date(allFilterTimeframe.value.x_axis.min);
|
||||
endTime.value = new Date(allFilterTimeframe.value.x_axis.max);
|
||||
startTime.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
endTime.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
selectArea.value = [0, selectRange.value];
|
||||
resizeMask(chart.value);
|
||||
}
|
||||
@@ -247,7 +247,7 @@ function resizeRightMask(chartInstance, to) {
|
||||
* Creates and renders the Chart.js area chart for timeframe data.
|
||||
*/
|
||||
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 maxX = timeFrameData.value[timeFrameData.value.length - 1]?.x;
|
||||
|
||||
@@ -403,10 +403,10 @@ onMounted(() => {
|
||||
// Slider
|
||||
selectArea.value = [0, selectRange.value];
|
||||
// Calendar
|
||||
startMinDate.value = new Date(allFilterTimeframe.value.x_axis.min);
|
||||
startMaxDate.value = new Date(allFilterTimeframe.value.x_axis.max);
|
||||
endMinDate.value = new Date(allFilterTimeframe.value.x_axis.min);
|
||||
endMaxDate.value = new Date(allFilterTimeframe.value.x_axis.max);
|
||||
startMinDate.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
startMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
endMinDate.value = new Date(filterTimeframe.value.x_axis.min);
|
||||
endMaxDate.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
// Set the calendar range to match the timeline range
|
||||
startTime.value = startMinDate.value;
|
||||
endTime.value = startMaxDate.value;
|
||||
|
||||
@@ -146,8 +146,8 @@ const {
|
||||
infinit404,
|
||||
baseInfiniteStart,
|
||||
baseTraces,
|
||||
allBaseTraceTaskSeq,
|
||||
allBaseCase,
|
||||
baseTraceTaskSeq,
|
||||
baseCases,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
|
||||
@@ -240,7 +240,7 @@ const caseData = computed(() => {
|
||||
});
|
||||
|
||||
const columnData = computed(() => {
|
||||
const data = cloneDeep(allBaseCase.value);
|
||||
const data = cloneDeep(baseCases.value);
|
||||
let result = [
|
||||
{ field: "id", header: "Case Id" },
|
||||
{ field: "started_at", header: "Start time" },
|
||||
@@ -378,7 +378,7 @@ function setNodesData() {
|
||||
// Clear nodes to prevent accumulation on each render
|
||||
processMap.value.nodes = [];
|
||||
// Populate nodes with data returned from the API call
|
||||
allBaseTraceTaskSeq.value.forEach((node, index) => {
|
||||
baseTraceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
@@ -398,7 +398,7 @@ function setNodesData() {
|
||||
*/
|
||||
function setEdgesData() {
|
||||
processMap.value.edges = [];
|
||||
allBaseTraceTaskSeq.value.forEach((edge, index) => {
|
||||
baseTraceTaskSeq.value.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
@@ -431,7 +431,7 @@ function createCy() {
|
||||
function handleScroll(event) {
|
||||
if (
|
||||
infinitMaxItems.value ||
|
||||
allBaseCase.value.length < 20 ||
|
||||
baseCases.value.length < 20 ||
|
||||
infiniteFinish.value === false
|
||||
)
|
||||
return;
|
||||
@@ -452,7 +452,7 @@ async function fetchData() {
|
||||
infiniteFinish.value = false;
|
||||
baseInfiniteStart.value += 20;
|
||||
await allMapDataStore.getBaseTraceDetail();
|
||||
infiniteData.value = [...infiniteData.value, ...allBaseCase.value];
|
||||
infiniteData.value = [...infiniteData.value, ...baseCases.value];
|
||||
infiniteFinish.value = true;
|
||||
isLoading.value = false;
|
||||
} catch (error) {
|
||||
|
||||
@@ -128,7 +128,7 @@ const {
|
||||
infiniteStart,
|
||||
traceId,
|
||||
traces,
|
||||
allTraceTaskSeq,
|
||||
traceTaskSeq,
|
||||
infiniteFirstCases,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
@@ -261,7 +261,7 @@ function setNodesData() {
|
||||
// Clear nodes to prevent accumulation on each render
|
||||
processMap.value.nodes = [];
|
||||
// Populate nodes with data returned from the API call
|
||||
allTraceTaskSeq.value.forEach((node, index) => {
|
||||
traceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
@@ -281,7 +281,7 @@ function setNodesData() {
|
||||
*/
|
||||
function setEdgesData() {
|
||||
processMap.value.edges = [];
|
||||
allTraceTaskSeq.value.forEach((edge, index) => {
|
||||
traceTaskSeq.value.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
|
||||
@@ -167,7 +167,7 @@ import getMoment from "moment";
|
||||
const route = useRoute();
|
||||
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { logId, allStats, createFilterId } = storeToRefs(allMapDataStore);
|
||||
const { logId, stats, createFilterId } = storeToRefs(allMapDataStore);
|
||||
|
||||
const isPanel = ref(false);
|
||||
const statData = ref(null);
|
||||
@@ -184,37 +184,37 @@ function getPercentLabel(val) {
|
||||
|
||||
/** Transforms raw stats into display-ready format with localized numbers and time labels. */
|
||||
function getStatData() {
|
||||
if (!allStats.value) return;
|
||||
if (!stats.value) return;
|
||||
statData.value = {
|
||||
cases: {
|
||||
count: allStats.value.cases.count.toLocaleString("en-US"),
|
||||
total: allStats.value.cases.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.cases.ratio),
|
||||
count: stats.value.cases.count.toLocaleString("en-US"),
|
||||
total: stats.value.cases.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.cases.ratio),
|
||||
},
|
||||
traces: {
|
||||
count: allStats.value.traces.count.toLocaleString("en-US"),
|
||||
total: allStats.value.traces.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.traces.ratio),
|
||||
count: stats.value.traces.count.toLocaleString("en-US"),
|
||||
total: stats.value.traces.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.traces.ratio),
|
||||
},
|
||||
task_instances: {
|
||||
count: allStats.value.task_instances.count.toLocaleString("en-US"),
|
||||
total: allStats.value.task_instances.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.task_instances.ratio),
|
||||
count: stats.value.task_instances.count.toLocaleString("en-US"),
|
||||
total: stats.value.task_instances.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.task_instances.ratio),
|
||||
},
|
||||
tasks: {
|
||||
count: allStats.value.tasks.count.toLocaleString("en-US"),
|
||||
total: allStats.value.tasks.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.tasks.ratio),
|
||||
count: stats.value.tasks.count.toLocaleString("en-US"),
|
||||
total: stats.value.tasks.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.tasks.ratio),
|
||||
},
|
||||
started_at: getMoment(allStats.value.started_at).format("YYYY-MM-DD HH:mm"),
|
||||
completed_at: getMoment(allStats.value.completed_at).format(
|
||||
started_at: getMoment(stats.value.started_at).format("YYYY-MM-DD HH:mm"),
|
||||
completed_at: getMoment(stats.value.completed_at).format(
|
||||
"YYYY-MM-DD HH:mm",
|
||||
),
|
||||
case_duration: {
|
||||
min: getTimeLabel(allStats.value.case_duration.min),
|
||||
max: getTimeLabel(allStats.value.case_duration.max),
|
||||
average: getTimeLabel(allStats.value.case_duration.average),
|
||||
median: getTimeLabel(allStats.value.case_duration.median),
|
||||
min: getTimeLabel(stats.value.case_duration.min),
|
||||
max: getTimeLabel(stats.value.case_duration.max),
|
||||
average: getTimeLabel(stats.value.case_duration.average),
|
||||
median: getTimeLabel(stats.value.case_duration.median),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import cytoscape from "cytoscape";
|
||||
import dagre from "cytoscape-dagre";
|
||||
import popper from "cytoscape-popper";
|
||||
import draggable from "vuedraggable";
|
||||
import VueSweetalert2 from "vue-sweetalert2";
|
||||
|
||||
// import CSS
|
||||
import "./assets/main.css";
|
||||
@@ -66,6 +67,7 @@ cytoscape.use(popper((ref) => ref));
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(VueSweetalert2);
|
||||
app.use(ToastPlugin, {
|
||||
// use `this.$toast` in Vue.js
|
||||
position: "bottom",
|
||||
@@ -105,7 +107,7 @@ async function enableMocking() {
|
||||
const { http, HttpResponse } = await import("msw");
|
||||
const { worker } = await import("./mocks/browser.js");
|
||||
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.__msw__ = { http, HttpResponse };
|
||||
}
|
||||
|
||||
+63
-2011
File diff suppressed because it is too large
Load Diff
+2107
-2107
File diff suppressed because it is too large
Load Diff
@@ -1,249 +1,249 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"label": "a",
|
||||
"occurrences": 187,
|
||||
"occurrence_ratio": 0.10048361096184846,
|
||||
"cases": 184,
|
||||
"case_ratio": 0.7301587301587301
|
||||
},
|
||||
{
|
||||
"label": "d",
|
||||
"occurrences": 241,
|
||||
"occurrence_ratio": 0.12950026867275657,
|
||||
"cases": 241,
|
||||
"case_ratio": 0.9563492063492064
|
||||
},
|
||||
{
|
||||
"label": "e",
|
||||
"occurrences": 249,
|
||||
"occurrence_ratio": 0.1337990327780763,
|
||||
"cases": 249,
|
||||
"case_ratio": 0.9880952380952381
|
||||
},
|
||||
{
|
||||
"label": "f",
|
||||
"occurrences": 249,
|
||||
"occurrence_ratio": 0.1337990327780763,
|
||||
"cases": 249,
|
||||
"case_ratio": 0.9880952380952381
|
||||
},
|
||||
{
|
||||
"label": "g",
|
||||
"occurrences": 253,
|
||||
"occurrence_ratio": 0.13594841483073616,
|
||||
"cases": 250,
|
||||
"case_ratio": 0.9920634920634921
|
||||
},
|
||||
{
|
||||
"label": "h",
|
||||
"occurrences": 163,
|
||||
"occurrence_ratio": 0.08758731864588931,
|
||||
"cases": 163,
|
||||
"case_ratio": 0.6468253968253969
|
||||
},
|
||||
{
|
||||
"label": "i",
|
||||
"occurrences": 185,
|
||||
"occurrence_ratio": 0.09940891993551854,
|
||||
"cases": 175,
|
||||
"case_ratio": 0.6944444444444444
|
||||
},
|
||||
{
|
||||
"label": "l",
|
||||
"occurrences": 182,
|
||||
"occurrence_ratio": 0.09779688339602365,
|
||||
"cases": 182,
|
||||
"case_ratio": 0.7222222222222222
|
||||
},
|
||||
{
|
||||
"label": "c",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.025792584631918324,
|
||||
"cases": 48,
|
||||
"case_ratio": 0.19047619047619047
|
||||
},
|
||||
{
|
||||
"label": "k",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.025792584631918324,
|
||||
"cases": 48,
|
||||
"case_ratio": 0.19047619047619047
|
||||
},
|
||||
{
|
||||
"label": "b",
|
||||
"occurrences": 34,
|
||||
"occurrence_ratio": 0.018269747447608814,
|
||||
"cases": 32,
|
||||
"case_ratio": 0.12698412698412698
|
||||
},
|
||||
{
|
||||
"label": "j",
|
||||
"occurrences": 22,
|
||||
"occurrence_ratio": 0.011821601289629231,
|
||||
"cases": 22,
|
||||
"case_ratio": 0.0873015873015873
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"label": "a",
|
||||
"occurrences": 175,
|
||||
"occurrence_ratio": 0.6944444444444444,
|
||||
"sinks": [
|
||||
"k",
|
||||
"l"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "c",
|
||||
"occurrences": 45,
|
||||
"occurrence_ratio": 0.17857142857142858,
|
||||
"sinks": [
|
||||
"k"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "b",
|
||||
"occurrences": 32,
|
||||
"occurrence_ratio": 0.12698412698412698,
|
||||
"sinks": [
|
||||
"k",
|
||||
"j",
|
||||
"l"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sinks": [
|
||||
{
|
||||
"label": "l",
|
||||
"occurrences": 182,
|
||||
"occurrence_ratio": 0.7222222222222222,
|
||||
"sources": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "k",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.19047619047619047,
|
||||
"sources": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "j",
|
||||
"occurrences": 22,
|
||||
"occurrence_ratio": 0.0873015873015873,
|
||||
"sources": [
|
||||
"b"
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeframe": {
|
||||
"data": [
|
||||
{
|
||||
"x": "2022-01-21T03:21:48Z",
|
||||
"y": 347
|
||||
},
|
||||
{
|
||||
"x": "2022-02-26T08:13:24Z",
|
||||
"y": 426
|
||||
},
|
||||
{
|
||||
"x": "2022-04-03T13:05:00Z",
|
||||
"y": 394
|
||||
},
|
||||
{
|
||||
"x": "2022-05-09T17:56:36Z",
|
||||
"y": 375
|
||||
},
|
||||
{
|
||||
"x": "2022-06-14T22:48:12Z",
|
||||
"y": 431
|
||||
},
|
||||
{
|
||||
"x": "2022-07-21T03:39:48Z",
|
||||
"y": 393
|
||||
},
|
||||
{
|
||||
"x": "2022-08-26T08:31:24Z",
|
||||
"y": 284
|
||||
},
|
||||
{
|
||||
"x": "2022-10-01T13:23:00Z",
|
||||
"y": 359
|
||||
},
|
||||
{
|
||||
"x": "2022-11-06T18:14:36Z",
|
||||
"y": 386
|
||||
},
|
||||
{
|
||||
"x": "2022-12-12T23:06:12Z",
|
||||
"y": 327
|
||||
}
|
||||
"tasks": [
|
||||
{
|
||||
"label": "a",
|
||||
"occurrences": 187,
|
||||
"occurrence_ratio": 0.10048361096184846,
|
||||
"cases": 184,
|
||||
"case_ratio": 0.7301587301587301
|
||||
},
|
||||
{
|
||||
"label": "d",
|
||||
"occurrences": 241,
|
||||
"occurrence_ratio": 0.12950026867275657,
|
||||
"cases": 241,
|
||||
"case_ratio": 0.9563492063492064
|
||||
},
|
||||
{
|
||||
"label": "e",
|
||||
"occurrences": 249,
|
||||
"occurrence_ratio": 0.1337990327780763,
|
||||
"cases": 249,
|
||||
"case_ratio": 0.9880952380952381
|
||||
},
|
||||
{
|
||||
"label": "f",
|
||||
"occurrences": 249,
|
||||
"occurrence_ratio": 0.1337990327780763,
|
||||
"cases": 249,
|
||||
"case_ratio": 0.9880952380952381
|
||||
},
|
||||
{
|
||||
"label": "g",
|
||||
"occurrences": 253,
|
||||
"occurrence_ratio": 0.13594841483073616,
|
||||
"cases": 250,
|
||||
"case_ratio": 0.9920634920634921
|
||||
},
|
||||
{
|
||||
"label": "h",
|
||||
"occurrences": 163,
|
||||
"occurrence_ratio": 0.08758731864588931,
|
||||
"cases": 163,
|
||||
"case_ratio": 0.6468253968253969
|
||||
},
|
||||
{
|
||||
"label": "i",
|
||||
"occurrences": 185,
|
||||
"occurrence_ratio": 0.09940891993551854,
|
||||
"cases": 175,
|
||||
"case_ratio": 0.6944444444444444
|
||||
},
|
||||
{
|
||||
"label": "l",
|
||||
"occurrences": 182,
|
||||
"occurrence_ratio": 0.09779688339602365,
|
||||
"cases": 182,
|
||||
"case_ratio": 0.7222222222222222
|
||||
},
|
||||
{
|
||||
"label": "c",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.025792584631918324,
|
||||
"cases": 48,
|
||||
"case_ratio": 0.19047619047619047
|
||||
},
|
||||
{
|
||||
"label": "k",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.025792584631918324,
|
||||
"cases": 48,
|
||||
"case_ratio": 0.19047619047619047
|
||||
},
|
||||
{
|
||||
"label": "b",
|
||||
"occurrences": 34,
|
||||
"occurrence_ratio": 0.018269747447608814,
|
||||
"cases": 32,
|
||||
"case_ratio": 0.12698412698412698
|
||||
},
|
||||
{
|
||||
"label": "j",
|
||||
"occurrences": 22,
|
||||
"occurrence_ratio": 0.011821601289629231,
|
||||
"cases": 22,
|
||||
"case_ratio": 0.0873015873015873
|
||||
}
|
||||
],
|
||||
"x_axis": {
|
||||
"min": "2022-01-03T00:56:00Z",
|
||||
"max": "2022-12-31T01:32:00Z"
|
||||
"sources": [
|
||||
{
|
||||
"label": "a",
|
||||
"occurrences": 175,
|
||||
"occurrence_ratio": 0.6944444444444444,
|
||||
"sinks": [
|
||||
"k",
|
||||
"l"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "c",
|
||||
"occurrences": 45,
|
||||
"occurrence_ratio": 0.17857142857142858,
|
||||
"sinks": [
|
||||
"k"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "b",
|
||||
"occurrences": 32,
|
||||
"occurrence_ratio": 0.12698412698412698,
|
||||
"sinks": [
|
||||
"k",
|
||||
"j",
|
||||
"l"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sinks": [
|
||||
{
|
||||
"label": "l",
|
||||
"occurrences": 182,
|
||||
"occurrence_ratio": 0.7222222222222222,
|
||||
"sources": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "k",
|
||||
"occurrences": 48,
|
||||
"occurrence_ratio": 0.19047619047619047,
|
||||
"sources": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "j",
|
||||
"occurrences": 22,
|
||||
"occurrence_ratio": 0.0873015873015873,
|
||||
"sources": [
|
||||
"b"
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeframe": {
|
||||
"data": [
|
||||
{
|
||||
"x": "2022-01-21T03:21:48",
|
||||
"y": 347
|
||||
},
|
||||
{
|
||||
"x": "2022-02-26T08:13:24",
|
||||
"y": 426
|
||||
},
|
||||
{
|
||||
"x": "2022-04-03T13:05:00",
|
||||
"y": 394
|
||||
},
|
||||
{
|
||||
"x": "2022-05-09T17:56:36",
|
||||
"y": 375
|
||||
},
|
||||
{
|
||||
"x": "2022-06-14T22:48:12",
|
||||
"y": 431
|
||||
},
|
||||
{
|
||||
"x": "2022-07-21T03:39:48",
|
||||
"y": 393
|
||||
},
|
||||
{
|
||||
"x": "2022-08-26T08:31:24",
|
||||
"y": 284
|
||||
},
|
||||
{
|
||||
"x": "2022-10-01T13:23:00",
|
||||
"y": 359
|
||||
},
|
||||
{
|
||||
"x": "2022-11-06T18:14:36",
|
||||
"y": 386
|
||||
},
|
||||
{
|
||||
"x": "2022-12-12T23:06:12",
|
||||
"y": 327
|
||||
}
|
||||
],
|
||||
"x_axis": {
|
||||
"min": "2022-01-03T00:56:00",
|
||||
"max": "2022-12-31T01:32:00"
|
||||
},
|
||||
"y_axis": {
|
||||
"min": 0,
|
||||
"max": 431
|
||||
}
|
||||
},
|
||||
"y_axis": {
|
||||
"min": 0,
|
||||
"max": 431
|
||||
}
|
||||
},
|
||||
"traces": [
|
||||
{
|
||||
"id": 1,
|
||||
"count": 95
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"count": 74
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"count": 45
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"count": 22
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"count": 8
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"attrs": []
|
||||
"traces": [
|
||||
{
|
||||
"id": 1,
|
||||
"count": 95
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"count": 74
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"count": 45
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"count": 22
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"count": 8
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"attrs": []
|
||||
}
|
||||
|
||||
+916
-916
File diff suppressed because it is too large
Load Diff
@@ -12,141 +12,141 @@
|
||||
"cases": [
|
||||
{
|
||||
"id": "H00564053",
|
||||
"started_at": "2022-01-03T00:56:00Z",
|
||||
"completed_at": "2022-01-12T02:29:00Z",
|
||||
"started_at": "2022-01-03T00:56:00",
|
||||
"completed_at": "2022-01-12T02:29:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00723931",
|
||||
"started_at": "2022-01-04T17:51:00Z",
|
||||
"completed_at": "2022-01-17T10:00:00Z",
|
||||
"started_at": "2022-01-04T17:51:00",
|
||||
"completed_at": "2022-01-17T10:00:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00542949",
|
||||
"started_at": "2022-01-10T19:05:00Z",
|
||||
"completed_at": "2022-01-28T09:38:00Z",
|
||||
"started_at": "2022-01-10T19:05:00",
|
||||
"completed_at": "2022-01-28T09:38:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00320575",
|
||||
"started_at": "2022-01-12T21:35:00Z",
|
||||
"completed_at": "2022-01-24T19:38:00Z",
|
||||
"started_at": "2022-01-12T21:35:00",
|
||||
"completed_at": "2022-01-24T19:38:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00565387",
|
||||
"started_at": "2022-01-20T20:30:00Z",
|
||||
"completed_at": "2022-02-06T10:57:00Z",
|
||||
"started_at": "2022-01-20T20:30:00",
|
||||
"completed_at": "2022-02-06T10:57:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00832338",
|
||||
"started_at": "2022-01-29T15:00:00Z",
|
||||
"completed_at": "2022-02-13T17:46:00Z",
|
||||
"started_at": "2022-01-29T15:00:00",
|
||||
"completed_at": "2022-02-13T17:46:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00525137",
|
||||
"started_at": "2022-02-05T23:26:00Z",
|
||||
"completed_at": "2022-02-14T19:47:00Z",
|
||||
"started_at": "2022-02-05T23:26:00",
|
||||
"completed_at": "2022-02-14T19:47:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00093124",
|
||||
"started_at": "2022-02-09T16:56:00Z",
|
||||
"completed_at": "2022-02-28T17:38:00Z",
|
||||
"started_at": "2022-02-09T16:56:00",
|
||||
"completed_at": "2022-02-28T17:38:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00657586",
|
||||
"started_at": "2022-02-14T20:07:00Z",
|
||||
"completed_at": "2022-02-28T15:21:00Z",
|
||||
"started_at": "2022-02-14T20:07:00",
|
||||
"completed_at": "2022-02-28T15:21:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00141668",
|
||||
"started_at": "2022-02-17T13:57:00Z",
|
||||
"completed_at": "2022-03-06T00:01:00Z",
|
||||
"started_at": "2022-02-17T13:57:00",
|
||||
"completed_at": "2022-03-06T00:01:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00493818",
|
||||
"started_at": "2022-02-20T19:54:00Z",
|
||||
"completed_at": "2022-03-05T05:06:00Z",
|
||||
"started_at": "2022-02-20T19:54:00",
|
||||
"completed_at": "2022-03-05T05:06:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00488827",
|
||||
"started_at": "2022-02-21T00:38:00Z",
|
||||
"completed_at": "2022-03-03T16:24:00Z",
|
||||
"started_at": "2022-02-21T00:38:00",
|
||||
"completed_at": "2022-03-03T16:24:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00874806",
|
||||
"started_at": "2022-02-24T15:15:00Z",
|
||||
"completed_at": "2022-03-12T01:12:00Z",
|
||||
"started_at": "2022-02-24T15:15:00",
|
||||
"completed_at": "2022-03-12T01:12:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00395448",
|
||||
"started_at": "2022-02-26T03:35:00Z",
|
||||
"completed_at": "2022-03-08T23:11:00Z",
|
||||
"started_at": "2022-02-26T03:35:00",
|
||||
"completed_at": "2022-03-08T23:11:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00414605",
|
||||
"started_at": "2022-02-26T17:11:00Z",
|
||||
"completed_at": "2022-03-10T08:50:00Z",
|
||||
"started_at": "2022-02-26T17:11:00",
|
||||
"completed_at": "2022-03-10T08:50:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00564269",
|
||||
"started_at": "2022-03-04T01:18:00Z",
|
||||
"completed_at": "2022-03-16T08:14:00Z",
|
||||
"started_at": "2022-03-04T01:18:00",
|
||||
"completed_at": "2022-03-16T08:14:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00729845",
|
||||
"started_at": "2022-03-05T09:29:00Z",
|
||||
"completed_at": "2022-03-17T15:25:00Z",
|
||||
"started_at": "2022-03-05T09:29:00",
|
||||
"completed_at": "2022-03-17T15:25:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00194115",
|
||||
"started_at": "2022-03-09T18:58:00Z",
|
||||
"completed_at": "2022-03-23T09:01:00Z",
|
||||
"started_at": "2022-03-09T18:58:00",
|
||||
"completed_at": "2022-03-23T09:01:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00517238",
|
||||
"started_at": "2022-03-21T06:30:00Z",
|
||||
"completed_at": "2022-04-05T05:27:00Z",
|
||||
"started_at": "2022-03-21T06:30:00",
|
||||
"completed_at": "2022-04-05T05:27:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
},
|
||||
{
|
||||
"id": "H00377237",
|
||||
"started_at": "2022-03-24T12:06:00Z",
|
||||
"completed_at": "2022-04-04T15:44:00Z",
|
||||
"started_at": "2022-03-24T12:06:00",
|
||||
"completed_at": "2022-04-04T15:44:00",
|
||||
"attributes": [],
|
||||
"facets": []
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"username": "testadmin",
|
||||
"name": "Test Admin",
|
||||
"is_admin": true,
|
||||
"is_active": true,
|
||||
"is_sso": false,
|
||||
"visits": 42,
|
||||
"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" },
|
||||
"has_data": true,
|
||||
"roles": [
|
||||
{ "code": "admin", "name": "Administrator" }
|
||||
]
|
||||
],
|
||||
"detail": {
|
||||
"visits": 42
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
||||
/** @module abbreviateNumber Duration abbreviation formatting. */
|
||||
|
||||
import formatDuration from "./formatDuration.js";
|
||||
|
||||
/**
|
||||
* Converts a total number of seconds into a human-readable abbreviated
|
||||
* duration string using days, hours, minutes, and seconds.
|
||||
@@ -17,7 +15,27 @@ import formatDuration from "./formatDuration.js";
|
||||
* or "0" if totalSeconds is zero.
|
||||
*/
|
||||
export default function abbreviateNumber(totalSeconds) {
|
||||
const parsed = Number.parseInt(totalSeconds);
|
||||
if (parsed === 0) return "0";
|
||||
return formatDuration(parsed, { separator: " " });
|
||||
let seconds = 0;
|
||||
let minutes = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
+160
-134
@@ -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] ",
|
||||
};
|
||||
/**
|
||||
* Shows a modal dialog prompting for a name and saves through the
|
||||
* given backend API function.
|
||||
* Shows a modal dialog to save a new filter with a user-provided name.
|
||||
*
|
||||
* @param {string} title - The dialog title.
|
||||
* @param {string} inputPlaceholder - The name input placeholder text.
|
||||
* @param {Function} save - Backend API function receiving the name.
|
||||
* @param {Function|null} [onCancel=null] - Callback invoked when the
|
||||
* dialog is cancelled or dismissed.
|
||||
* @returns {Promise<boolean>} True if saved, false otherwise.
|
||||
* @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.
|
||||
*/
|
||||
async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
|
||||
export async function saveFilter(addFilterId, next = null) {
|
||||
let fileName = "";
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
|
||||
const { value, isConfirmed } = await Swal.fire({
|
||||
title: title,
|
||||
title: "SAVE NEW FILTER",
|
||||
input: "text",
|
||||
inputPlaceholder: inputPlaceholder,
|
||||
inputPlaceholder: "Enter Filter Name.",
|
||||
inputValue: fileName,
|
||||
inputAttributes: {
|
||||
maxlength: 200,
|
||||
@@ -67,7 +64,7 @@ async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
|
||||
// Determine whether to redirect based on the return value
|
||||
if (isConfirmed) {
|
||||
// Save succeeded
|
||||
await save(fileName);
|
||||
await addFilterId(fileName);
|
||||
// Show save complete notification
|
||||
if (value) {
|
||||
// Example of value: yes
|
||||
@@ -76,34 +73,16 @@ async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
|
||||
// Clear the input field
|
||||
fileName = "";
|
||||
return true;
|
||||
} else {
|
||||
// Clicked cancel or outside the dialog; save failed.
|
||||
pageAdminStore.keepPreviousPage();
|
||||
|
||||
// Not every time we have nontrivial next value
|
||||
if (next !== null) {
|
||||
next(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// 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();
|
||||
|
||||
// Not every time we have nontrivial next value
|
||||
if (next !== null) {
|
||||
next(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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 pageAdminStore = usePageAdminStore();
|
||||
|
||||
await confirmLeave({
|
||||
const result = await Swal.fire({
|
||||
title: "SAVE YOUR FILTER?",
|
||||
html: "If you want to continue using this filter in any other page, please select [Yes].",
|
||||
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",
|
||||
iconColor: "#FF3366",
|
||||
reverseButtons: true,
|
||||
@@ -211,16 +132,31 @@ async function confirmLeave({
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await saveChanges();
|
||||
if (navigateAfterSave) {
|
||||
logOut ? logOut() : next(toPath);
|
||||
if (allMapDataStore.createFilterId) {
|
||||
await allMapDataStore.updateFilter();
|
||||
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") {
|
||||
if (beforeDismiss) beforeDismiss();
|
||||
discardTemp();
|
||||
// console.log('popup cancel case', );
|
||||
// Handle page admin issue
|
||||
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
|
||||
pageAdminStore.keepPreviousPage();
|
||||
|
||||
allMapDataStore.tempFilterId = null;
|
||||
logOut ? logOut() : next(toPath);
|
||||
} 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) {
|
||||
next(false);
|
||||
}
|
||||
@@ -235,16 +171,47 @@ async function confirmLeave({
|
||||
* @returns {Promise<boolean>} True if the rule was saved, false otherwise.
|
||||
*/
|
||||
export async function saveConformance(addConformanceCreateCheckId) {
|
||||
return promptSaveName(
|
||||
"SAVE NEW RULE",
|
||||
"Enter Rule Name.",
|
||||
addConformanceCreateCheckId,
|
||||
);
|
||||
let fileName = "";
|
||||
const { value, isConfirmed } = await Swal.fire({
|
||||
title: "SAVE NEW RULE",
|
||||
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
|
||||
* leaving the Conformance page. Handles confirm (save), cancel
|
||||
* (discard), and backdrop (stay) scenarios.
|
||||
* leaving the Conformance page. Delegates to helper functions for
|
||||
* confirm, cancel, and backdrop scenarios.
|
||||
*
|
||||
* @param {Function} next - Vue Router next() guard callback.
|
||||
* @param {Function} addConformanceCreateCheckId - Backend API function
|
||||
@@ -260,34 +227,93 @@ export async function leaveConformance(
|
||||
logOut,
|
||||
) {
|
||||
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?",
|
||||
saveChanges: async () => {
|
||||
if (
|
||||
conformanceStore.conformanceFilterCreateCheckId ||
|
||||
conformanceStore.conformanceLogCreateCheckId
|
||||
) {
|
||||
await conformanceStore.updateConformance();
|
||||
if (conformanceStore.isUpdateConformance) {
|
||||
await savedSuccessfully(conformanceStore.conformanceFileName);
|
||||
}
|
||||
} else {
|
||||
await saveConformance(addConformanceCreateCheckId);
|
||||
}
|
||||
},
|
||||
navigateAfterSave: false,
|
||||
beforeDismiss: null,
|
||||
discardTemp: () => {
|
||||
conformanceStore.conformanceFilterTempCheckId = null;
|
||||
conformanceStore.conformanceLogTempCheckId = null;
|
||||
},
|
||||
next: next,
|
||||
toPath: toPath,
|
||||
logOut: logOut,
|
||||
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 (
|
||||
conformanceStore.conformanceFilterCreateCheckId ||
|
||||
conformanceStore.conformanceLogCreateCheckId
|
||||
) {
|
||||
await conformanceStore.updateConformance();
|
||||
if (conformanceStore.isUpdateConformance) {
|
||||
await savedSuccessfully(conformanceStore.conformanceFileName);
|
||||
}
|
||||
} else {
|
||||
await saveConformance(addConformanceCreateCheckId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.conformanceLogTempCheckId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error modal for the first stage of upload validation failure.
|
||||
*
|
||||
|
||||
@@ -14,9 +14,10 @@ import "vue-toast-notification/dist/theme-sugar.css";
|
||||
* 401 errors are handled by the axios response interceptor
|
||||
* in api/client.js.
|
||||
*
|
||||
* @param {Object} error - The error object from the API call.
|
||||
* @param {string} toastMessage - The message to display in the toast.
|
||||
*/
|
||||
export default function apiError(toastMessage) {
|
||||
export default function apiError(error, toastMessage) {
|
||||
const $toast = useToast();
|
||||
$toast.default(toastMessage, { position: "bottom" });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -6,18 +6,30 @@
|
||||
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
||||
/** @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.
|
||||
*
|
||||
* Handles decimal numbers by formatting only the integer part. A comma
|
||||
* is inserted before every group of three characters counted from the
|
||||
* end of the integer part.
|
||||
* Handles decimal numbers by formatting only the integer part.
|
||||
*
|
||||
* @param {number} num - The number to format.
|
||||
* @returns {string} The formatted number string (e.g. "1,234.56").
|
||||
*/
|
||||
export default function numberLabel(num) {
|
||||
const parts = num.toString().split(".");
|
||||
parts[0] = parts[0].replace(/(.)(?=(?:.{3})+$)/g, "$1,");
|
||||
let parts = num.toString().split(".");
|
||||
parts[0] = formatNumberWithCommas(parts[0]);
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
+37
-22
@@ -8,8 +8,6 @@
|
||||
|
||||
import getMoment from "moment";
|
||||
|
||||
import formatDuration, { DURATION_UNITS } from "./formatDuration.js";
|
||||
|
||||
/**
|
||||
* Extends backend chart data with extrapolated boundary points for
|
||||
* line charts. Prepends a calculated minimum and appends a calculated
|
||||
@@ -243,8 +241,26 @@ export function getXIndex(data, xValue) {
|
||||
export function formatTime(seconds) {
|
||||
if (Number.isNaN(Number(seconds))) {
|
||||
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.
|
||||
@@ -253,23 +269,22 @@ export function formatTime(seconds) {
|
||||
* @returns {Array<string>} Array of truncated strings (e.g. "2d 3h").
|
||||
*/
|
||||
export function formatMaxTwo(times) {
|
||||
const secondsPerUnit = Object.fromEntries(
|
||||
DURATION_UNITS.map((unit) => [unit.symbol, unit.seconds]),
|
||||
);
|
||||
return times.map((time) => {
|
||||
// Match numbers and units (days, hours, minutes, seconds); assume
|
||||
// numbers have at most 10 digits.
|
||||
const units = time.match(/\d{1,10}[dhms]/g);
|
||||
if (!units) return "";
|
||||
const totalSeconds = units.reduce(
|
||||
(sum, unit) =>
|
||||
sum + Number.parseInt(unit) * secondsPerUnit[unit.slice(-1)],
|
||||
0,
|
||||
);
|
||||
return formatDuration(totalSeconds, {
|
||||
separator: " ",
|
||||
maxUnits: 2,
|
||||
alwaysSeconds: units.some((unit) => unit.endsWith("s")),
|
||||
});
|
||||
});
|
||||
const formattedTimes = [];
|
||||
for (let time of times) {
|
||||
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
|
||||
let units = time.match(/\d{1,10}[dhms]/g) || [];
|
||||
let formattedTime = "";
|
||||
let count = 0;
|
||||
|
||||
// Keep only the two largest units
|
||||
for (let unit of units) {
|
||||
if (count >= 2) {
|
||||
break;
|
||||
}
|
||||
formattedTime += unit + " ";
|
||||
count++;
|
||||
}
|
||||
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
|
||||
}
|
||||
return formattedTimes;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
* @returns {Array<string|number>} The sorted array (same reference).
|
||||
*/
|
||||
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" });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+113
-43
@@ -8,8 +8,6 @@
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import { pickDurationUnit } from "./formatDuration.js";
|
||||
|
||||
/** @constant {number} Number of decimal places for formatted time values. */
|
||||
const TOFIXED_DECIMAL = 1;
|
||||
|
||||
@@ -38,11 +36,34 @@ export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
|
||||
* character ("d", "h", "m", or "s") and the converted time value.
|
||||
*/
|
||||
const getTimeUnitAndValueToUse = (secondToDecide) => {
|
||||
const unit = pickDurationUnit(secondToDecide, true);
|
||||
return {
|
||||
unitToUse: unit.symbol,
|
||||
timeValue: secondToDecide / unit.seconds,
|
||||
};
|
||||
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 {
|
||||
unitToUse: "d",
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -89,12 +89,27 @@ function scaledTimeLabel(second, fixedNumber, suffixes) {
|
||||
* "30 mins", "5 sec").
|
||||
*/
|
||||
export function getTimeLabel(second, fixedNumber = 0) {
|
||||
return scaledTimeLabel(second, fixedNumber, {
|
||||
d: " days",
|
||||
h: " hrs",
|
||||
m: " mins",
|
||||
s: " sec",
|
||||
});
|
||||
const day = 24 * 60 * 60;
|
||||
const hour = 60 * 60;
|
||||
const minutes = 60;
|
||||
|
||||
// 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").
|
||||
*/
|
||||
export function simpleTimeLabel(second, fixedNumber = 0) {
|
||||
return scaledTimeLabel(second, fixedNumber, {
|
||||
d: "d",
|
||||
h: "h",
|
||||
m: "m",
|
||||
s: "s",
|
||||
});
|
||||
const day = 24 * 60 * 60;
|
||||
const hour = 60 * 60;
|
||||
const minutes = 60;
|
||||
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) + "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
|
||||
@@ -128,9 +155,52 @@ export function simpleTimeLabel(second, fixedNumber = 0) {
|
||||
* @returns {string} The formatted string (e.g. "1.5d", "6.8h").
|
||||
*/
|
||||
export function followTimeLabel(second, max, fixedNumber = 0) {
|
||||
const unit = pickDurationUnit(max, true);
|
||||
const value = second / unit.seconds;
|
||||
return value.toFixed(value === 0 ? 0 : fixedNumber) + unit.symbol;
|
||||
const day = 24 * 60 * 60;
|
||||
const hour = 60 * 60;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -103,7 +103,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
const customizedResponseData = await this.customizeAllUserList(response.data);
|
||||
this.allUserAccountList = await this.moveCurrentLoginUserToFirstRow(customizedResponseData);
|
||||
} 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);
|
||||
}
|
||||
} 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);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to delete the account.');
|
||||
apiError(error, 'Failed to delete the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -191,7 +191,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to edit the account.');
|
||||
apiError(error, 'Failed to edit the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -207,7 +207,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to edit name of account.');
|
||||
apiError(error, 'Failed to edit name of account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -224,7 +224,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to edit password of account.');
|
||||
apiError(error, 'Failed to edit password of account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -239,7 +239,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
const response = await apiClient.put(apiAddRole);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to add role to the account.');
|
||||
apiError(error, 'Failed to add role to the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -254,7 +254,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
const response = await apiClient.delete(apiDeleteRole);
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError('Failed to delete a role from the account.');
|
||||
apiError(error, 'Failed to delete a role from the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
+52
-10
@@ -71,18 +71,57 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
processMap: (state) => {
|
||||
return state.allProcessMap;
|
||||
},
|
||||
bpmn: (state) => {
|
||||
return state.allBpmn;
|
||||
},
|
||||
stats: (state) => {
|
||||
return state.allStats;
|
||||
},
|
||||
insights: (state) => {
|
||||
return state.allInsights;
|
||||
},
|
||||
traces: (state) => {
|
||||
return [...state.allTrace].sort((x, y) => x.id - y.id);
|
||||
},
|
||||
baseTraces: (state) => {
|
||||
return [...state.allBaseTrace].sort((x, y) => x.id - y.id);
|
||||
},
|
||||
cases: (state) => {
|
||||
return state.allCase;
|
||||
},
|
||||
baseCases: (state) => {
|
||||
return state.allBaseCase;
|
||||
},
|
||||
infiniteFirstCases: (state) => {
|
||||
if (state.infiniteStart === 0) return state.allCase;
|
||||
},
|
||||
BaseInfiniteFirstCases: (state) => {
|
||||
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) => {
|
||||
if (state.allFilterAttrs !== null) {
|
||||
return state.allFilterAttrs.map((att) => {
|
||||
@@ -116,6 +155,9 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
}
|
||||
return [];
|
||||
},
|
||||
allFunnels: (state) => {
|
||||
return state.allFunnelData;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
@@ -130,7 +172,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.allStats = response.data.stats;
|
||||
this.allInsights = response.data.insights;
|
||||
} 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;
|
||||
}
|
||||
} 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;
|
||||
return;
|
||||
}
|
||||
apiError("Failed to load the Trace Detail.");
|
||||
apiError(error, "Failed to load the Trace Detail.");
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -236,7 +278,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.infinite404 = 404;
|
||||
return;
|
||||
}
|
||||
apiError("Failed to load the Base Trace Detail.");
|
||||
apiError(error, "Failed to load the Base Trace Detail.");
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -267,7 +309,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.allFilterTimeframe.x_axis.max =
|
||||
max === null ? null : moment(max).format("YYYY/MM/DD HH:mm");
|
||||
} 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);
|
||||
this.hasResultRule = response.data.result;
|
||||
} 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);
|
||||
this.tempFilterId = response.data.id;
|
||||
} 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.tempFilterId = null;
|
||||
} 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.baseLogId = response.data.log?.id;
|
||||
} 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.tempFilterId = null;
|
||||
} catch (error) {
|
||||
apiError("Failed to update an Existing Filter.");
|
||||
apiError(error, "Failed to update an Existing Filter.");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export const useCompareStore = defineStore("compareStore", {
|
||||
const response = await apiClient.get(api);
|
||||
this.allCompareDashboardData = response.data;
|
||||
} 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;
|
||||
} catch (error) {
|
||||
apiError("Failed to load the Compare's States.");
|
||||
apiError(error, "Failed to load the Compare's States.");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
@@ -79,7 +79,7 @@ export const useCompareStore = defineStore("compareStore", {
|
||||
|
||||
if (file) return file.name;
|
||||
} catch (error) {
|
||||
apiError("Failed to load the Compare's file name.");
|
||||
apiError(error, "Failed to load the Compare's file name.");
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
@@ -91,9 +91,18 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
conformanceFileName: null, // File name displayed in the save success modal
|
||||
}),
|
||||
getters: {
|
||||
conformanceAllTasks: (state) => {
|
||||
return state.allConformanceTask;
|
||||
},
|
||||
conformanceTask: (state) => {
|
||||
return state.allConformanceTask.map((i) => i.label);
|
||||
},
|
||||
cfmSeqStart: (state) => {
|
||||
return state.allCfmSeqStart;
|
||||
},
|
||||
cfmSeqEnd: (state) => {
|
||||
return state.allCfmSeqEnd;
|
||||
},
|
||||
cfmPtEteWhole: (state) => {
|
||||
return state.allProcessingTime.end_to_end.whole;
|
||||
},
|
||||
@@ -148,6 +157,18 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
cfmCtEteSE: (state) => {
|
||||
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) => {
|
||||
if (state.allCases !== null) {
|
||||
return state.allCases.map((c) => {
|
||||
@@ -199,6 +220,12 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
}
|
||||
return [];
|
||||
},
|
||||
loopTraces: (state) => {
|
||||
return state.allLoopTraces;
|
||||
},
|
||||
loopTaskSeq: (state) => {
|
||||
return state.allLoopTaskSeq;
|
||||
},
|
||||
loopCases: (state) => {
|
||||
if (state.allLoopCases !== null) {
|
||||
return state.allLoopCases.map((c) => {
|
||||
@@ -249,7 +276,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.allWaitingTime = response.data.waiting_time;
|
||||
this.allCycleTime = response.data.cycle_time;
|
||||
} 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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
} 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;
|
||||
} catch (error) {
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance issue.",
|
||||
);
|
||||
}
|
||||
@@ -322,6 +350,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
return null;
|
||||
}
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance issue.",
|
||||
);
|
||||
return null;
|
||||
@@ -338,6 +367,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.allLoopTraces = response.data.traces;
|
||||
} catch (error) {
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance loop.",
|
||||
);
|
||||
}
|
||||
@@ -361,6 +391,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
return null;
|
||||
}
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance loop.",
|
||||
);
|
||||
return null;
|
||||
@@ -388,7 +419,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.conformanceLogTempCheckId = null;
|
||||
}
|
||||
} 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.conformanceFilterTempCheckId = null;
|
||||
} catch (error) {
|
||||
apiError("Failed to update an Existing Conformance.");
|
||||
apiError(error, "Failed to update an Existing Conformance.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -26,8 +26,8 @@ export interface Node {
|
||||
}
|
||||
|
||||
export interface NodePositions {
|
||||
[graphId: string]: {
|
||||
[direction: string]: Node[];
|
||||
[direction: string]: {
|
||||
[graphId: string]: Node[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,16 @@ export const useCytoscapeStore = defineStore('cytoscapeStore', {
|
||||
if (!this.nodePositions[this.currentGraphId][direction]) {
|
||||
this.nodePositions[this.currentGraphId][direction] = [];
|
||||
}
|
||||
const nodeToSave = this.nodePositions[this.currentGraphId][direction]
|
||||
.find(node => node.id === nodeId);
|
||||
if (nodeToSave) {
|
||||
nodeToSave.position = position;
|
||||
// If this graph's data was previously stored in localStorage
|
||||
if (localStorage.getItem(SAVE_KEY_NAME)) {
|
||||
const nodeToSave = this.nodePositions[this.currentGraphId][direction]
|
||||
.find(node => node.id === nodeId);
|
||||
if (nodeToSave) {
|
||||
nodeToSave.position = position;
|
||||
} else {
|
||||
this.nodePositions[this.currentGraphId][direction]
|
||||
.push({ id: nodeId, position: position });
|
||||
}
|
||||
} else {
|
||||
this.nodePositions[this.currentGraphId][direction]
|
||||
.push({ id: nodeId, position: position });
|
||||
|
||||
+9
-9
@@ -146,7 +146,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
: null;
|
||||
});
|
||||
} 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 {
|
||||
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);
|
||||
this.allUploadDetail = response.data.preview;
|
||||
} 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);
|
||||
} else {
|
||||
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;
|
||||
await this.fetchAllFiles();
|
||||
} catch (error) {
|
||||
apiError("Failed to rename.");
|
||||
apiError(error, "Failed to rename.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -262,7 +262,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
);
|
||||
this.allDependentsData = response.data;
|
||||
} 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 deleteSuccess();
|
||||
} catch (error) {
|
||||
apiError("Failed to delete.");
|
||||
apiError(error, "Failed to delete.");
|
||||
} finally {
|
||||
loading.isLoading = false;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
try {
|
||||
await apiClient.delete(api);
|
||||
} catch (error) {
|
||||
apiError("Failed to Remove a Deletion Record.");
|
||||
apiError(error, "Failed to Remove a Deletion Record.");
|
||||
} finally {
|
||||
loading.isLoading = false;
|
||||
}
|
||||
@@ -325,7 +325,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
link.click();
|
||||
globalThis.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
apiError("Failed to download.");
|
||||
apiError(error, "Failed to download.");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export const useLoginStore = defineStore("loginStore", {
|
||||
|
||||
this.userData = response.data;
|
||||
} catch (error) {
|
||||
apiError("Failed to load user data.");
|
||||
apiError(error, "Failed to load user data.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
+3
-1
@@ -5,11 +5,13 @@
|
||||
// imacat.yang@dsp.im (imacat), 2023/9/23
|
||||
// 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 piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
export default pinia;
|
||||
|
||||
@@ -163,8 +163,8 @@ export const useMapPathStore = defineStore('mapPathStore', {
|
||||
}
|
||||
},
|
||||
async createInsightWithPath() {
|
||||
const { allInsights } = useAllMapDataStore();
|
||||
this.insights = { ...allInsights };
|
||||
const { insights } = useAllMapDataStore();
|
||||
this.insights = { ...insights };
|
||||
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
|
||||
.filter(function (elem) {
|
||||
return elem.data('label').toLowerCase() === 'start';
|
||||
@@ -182,8 +182,8 @@ export const useMapPathStore = defineStore('mapPathStore', {
|
||||
});
|
||||
// Depth First Search from the starting node
|
||||
this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []);
|
||||
const { allInsights } = useAllMapDataStore();
|
||||
this.insights = { ...allInsights };
|
||||
const { insights } = useAllMapDataStore();
|
||||
this.insights = { ...insights };
|
||||
this.matchGraphPathWithInsightsPath();
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ export const usePerformanceStore = defineStore("performanceStore", {
|
||||
const response = await apiClient.get(api);
|
||||
this.allPerformanceData = response.data;
|
||||
} catch (error) {
|
||||
apiError("Failed to load the Performance.");
|
||||
apiError(error, "Failed to load the Performance.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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 () => {
|
||||
await acctMgmtStore.getUserDetail(currentViewingUser.value.username);
|
||||
visitTime.value = currentViewingUser.value.visits ?? 0;
|
||||
visitTime.value = currentViewingUser.value.detail?.visits ?? 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -86,23 +86,23 @@
|
||||
></SidebarView>
|
||||
<SidebarState
|
||||
v-model:visible="sidebarState"
|
||||
:insights="allInsights"
|
||||
:stats="allStats"
|
||||
:insights="insights"
|
||||
:stats="stats"
|
||||
></SidebarState>
|
||||
<SidebarTraces
|
||||
v-model:visible="sidebarTraces"
|
||||
:cases="allCase"
|
||||
:cases="cases"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="tracesViewRef"
|
||||
>
|
||||
</SidebarTraces>
|
||||
<SidebarFilter
|
||||
v-model:visible="sidebarFilter"
|
||||
:filterTasks="allFilterTask"
|
||||
:filterStartToEnd="allFilterStartToEnd"
|
||||
:filterEndToStart="allFilterEndToStart"
|
||||
:filterTimeframe="allFilterTimeframe"
|
||||
:filterTrace="allFilterTrace"
|
||||
:filterTasks="filterTasks"
|
||||
:filterStartToEnd="filterStartToEnd"
|
||||
:filterEndToStart="filterEndToStart"
|
||||
:filterTimeframe="filterTimeframe"
|
||||
:filterTrace="filterTrace"
|
||||
@submit-all="createCy(mapType)"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="sidebarFilterRefComp"
|
||||
@@ -138,7 +138,7 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport(true);
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
to.meta.file = conformanceStore.routeFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -176,25 +176,25 @@ const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const {
|
||||
processMap,
|
||||
allBpmn,
|
||||
allStats,
|
||||
allInsights,
|
||||
bpmn,
|
||||
stats,
|
||||
insights,
|
||||
traceId,
|
||||
traces,
|
||||
baseTraces,
|
||||
baseTraceId,
|
||||
allFilterTask,
|
||||
allFilterStartToEnd,
|
||||
allFilterEndToStart,
|
||||
allFilterTimeframe,
|
||||
allFilterTrace,
|
||||
filterTasks,
|
||||
filterStartToEnd,
|
||||
filterEndToStart,
|
||||
filterTimeframe,
|
||||
filterTrace,
|
||||
temporaryData,
|
||||
isRuleData,
|
||||
ruleData,
|
||||
logId,
|
||||
baseLogId,
|
||||
createFilterId,
|
||||
allCase,
|
||||
cases,
|
||||
postRuleData,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
@@ -401,7 +401,7 @@ function setNodesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.nodes = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
mapSource.vertices.forEach((node) => {
|
||||
switch (node.type) {
|
||||
case "gateway":
|
||||
@@ -485,7 +485,7 @@ function setEdgesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.edges = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
mapSource.edges.forEach((edge) => {
|
||||
mapData.edges.push({
|
||||
data: {
|
||||
@@ -510,7 +510,7 @@ function setEdgesData(mapData) {
|
||||
async function createCy(type) {
|
||||
const graphId = document.getElementById("cy");
|
||||
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) {
|
||||
setNodesData(mapData);
|
||||
|
||||
@@ -42,7 +42,7 @@ defineOptions({
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport();
|
||||
to.meta.file = await conformanceStore.allConformanceTempReportData?.file;
|
||||
to.meta.file = await conformanceStore.conformanceTempReportData?.file;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -68,7 +68,7 @@ const {
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceRuleData,
|
||||
allConformanceTempReportData,
|
||||
conformanceTempReportData,
|
||||
conformanceFileName,
|
||||
} = storeToRefs(conformanceStore);
|
||||
|
||||
|
||||
@@ -86,22 +86,22 @@
|
||||
></SidebarView>
|
||||
<SidebarState
|
||||
v-model:visible="sidebarState"
|
||||
:insights="allInsights"
|
||||
:stats="allStats"
|
||||
:insights="insights"
|
||||
:stats="stats"
|
||||
></SidebarState>
|
||||
<SidebarTraces
|
||||
v-model:visible="sidebarTraces"
|
||||
:cases="allCase"
|
||||
:cases="cases"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="tracesViewRef"
|
||||
></SidebarTraces>
|
||||
<SidebarFilter
|
||||
v-model:visible="sidebarFilter"
|
||||
:filterTasks="allFilterTask"
|
||||
:filterStartToEnd="allFilterStartToEnd"
|
||||
:filterEndToStart="allFilterEndToStart"
|
||||
:filterTimeframe="allFilterTimeframe"
|
||||
:filterTrace="allFilterTrace"
|
||||
:filterTasks="filterTasks"
|
||||
:filterStartToEnd="filterStartToEnd"
|
||||
:filterEndToStart="filterEndToStart"
|
||||
:filterTimeframe="filterTimeframe"
|
||||
:filterTrace="filterTrace"
|
||||
@submit-all="createCy(mapType)"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="sidebarFilterRefComp"
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport(true);
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
to.meta.file = conformanceStore.routeFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -175,25 +175,25 @@ const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const {
|
||||
processMap,
|
||||
allBpmn,
|
||||
allStats,
|
||||
allInsights,
|
||||
bpmn,
|
||||
stats,
|
||||
insights,
|
||||
traceId,
|
||||
traces,
|
||||
baseTraces,
|
||||
baseTraceId,
|
||||
allFilterTask,
|
||||
allFilterStartToEnd,
|
||||
allFilterEndToStart,
|
||||
allFilterTimeframe,
|
||||
allFilterTrace,
|
||||
filterTasks,
|
||||
filterStartToEnd,
|
||||
filterEndToStart,
|
||||
filterTimeframe,
|
||||
filterTrace,
|
||||
temporaryData,
|
||||
isRuleData,
|
||||
ruleData,
|
||||
logId,
|
||||
baseLogId,
|
||||
createFilterId,
|
||||
allCase,
|
||||
cases,
|
||||
postRuleData,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
@@ -398,7 +398,7 @@ function setNodesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.nodes = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
mapSource.vertices.forEach((node) => {
|
||||
switch (node.type) {
|
||||
case "gateway":
|
||||
@@ -477,7 +477,7 @@ function setEdgesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.edges = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
mapSource.edges.forEach((edge) => {
|
||||
mapData.edges.push({
|
||||
data: {
|
||||
@@ -499,7 +499,7 @@ function setEdgesData(mapData) {
|
||||
async function createCy(type) {
|
||||
const graphId = document.getElementById("cy");
|
||||
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) {
|
||||
setNodesData(mapData);
|
||||
|
||||
@@ -263,7 +263,7 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport(true);
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
to.meta.file = conformanceStore.routeFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
@@ -8,7 +8,6 @@ import { defineConfig } from "@playwright/test";
|
||||
export default defineConfig({
|
||||
testDir: "./specs",
|
||||
timeout: 30000,
|
||||
workers: 4,
|
||||
expect: { timeout: 5000 },
|
||||
use: {
|
||||
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 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -46,7 +46,9 @@ export default defineConfig(({ mode }) => {
|
||||
base: "/",
|
||||
resolve: {
|
||||
alias: {
|
||||
util: "util/",
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"/images": "src/assets/images",
|
||||
},
|
||||
},
|
||||
server: {
|
||||
@@ -59,7 +61,13 @@ export default defineConfig(({ mode }) => {
|
||||
port: 4173,
|
||||
proxy,
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["vue", "vue-router", "pinia", "axios"],
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true, // Enable @walletconnect/web3-provider which has some code in CommonJS
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
|
||||
Reference in New Issue
Block a user