Compare commits
12
Commits
55986a1965
...
00ff034e0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ff034e0e | ||
|
|
8b06094978 | ||
|
|
ab1eacc1c2 | ||
|
|
e5a6f7eaf2 | ||
|
|
c54557c11c | ||
|
|
7095db5ee9 | ||
|
|
217c18659b | ||
|
|
ba1f2f4ac9 | ||
|
|
834d69f686 | ||
|
|
c6da40953f | ||
|
|
b4a6ffe743 | ||
|
|
da2cdb240f |
Generated
+2
-132
@@ -27,14 +27,13 @@
|
||||
"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"
|
||||
},
|
||||
@@ -50,7 +49,6 @@
|
||||
"@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",
|
||||
@@ -3201,44 +3199,6 @@
|
||||
"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",
|
||||
@@ -3580,12 +3540,6 @@
|
||||
"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",
|
||||
@@ -5691,31 +5645,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -6242,6 +6171,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -6945,17 +6875,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -9042,34 +8961,6 @@
|
||||
"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",
|
||||
@@ -9324,11 +9215,6 @@
|
||||
"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",
|
||||
@@ -10643,14 +10529,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -11373,14 +11251,6 @@
|
||||
"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",
|
||||
|
||||
+1
-3
@@ -36,14 +36,13 @@
|
||||
"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"
|
||||
},
|
||||
@@ -59,7 +58,6 @@
|
||||
"@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="issueTraces"
|
||||
:taskSeq="taskSeq"
|
||||
:listTraces="allIssueTraces"
|
||||
:taskSeq="allTaskSeq"
|
||||
:cases="cases"
|
||||
:listNo="issuesNo"
|
||||
:traceId="traceId"
|
||||
@@ -369,8 +369,8 @@
|
||||
<MoreModal
|
||||
:listModal="loopModal"
|
||||
@closeModal="loopModal = $event"
|
||||
:listTraces="loopTraces"
|
||||
:taskSeq="loopTaskSeq"
|
||||
:listTraces="allLoopTraces"
|
||||
:taskSeq="allLoopTaskSeq"
|
||||
:cases="loopCases"
|
||||
:listNo="loopNo"
|
||||
:traceId="looptraceId"
|
||||
@@ -413,12 +413,12 @@ import emitter from "@/utils/emitter";
|
||||
|
||||
const conformanceStore = useConformanceStore();
|
||||
const {
|
||||
conformanceTempReportData,
|
||||
issueTraces,
|
||||
taskSeq,
|
||||
allConformanceTempReportData,
|
||||
allIssueTraces,
|
||||
allTaskSeq,
|
||||
cases,
|
||||
loopTraces,
|
||||
loopTaskSeq,
|
||||
allLoopTraces,
|
||||
allLoopTaskSeq,
|
||||
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 (issueTraces.value.length === 0) return;
|
||||
traceId.value = issueTraces.value[0].id;
|
||||
if (allIssueTraces.value.length === 0) return;
|
||||
traceId.value = allIssueTraces.value[0].id;
|
||||
firstCases.value = await conformanceStore.getConformanceTraceDetail(
|
||||
no,
|
||||
issueTraces.value[0].id,
|
||||
allIssueTraces.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 (loopTraces.value.length === 0) return;
|
||||
looptraceId.value = loopTraces.value[0].id;
|
||||
if (allLoopTraces.value.length === 0) return;
|
||||
looptraceId.value = allLoopTraces.value[0].id;
|
||||
loopFirstCases.value = await conformanceStore.getConformanceLoopsTraceDetail(
|
||||
no,
|
||||
loopTraces.value[0].id,
|
||||
allLoopTraces.value[0].id,
|
||||
0,
|
||||
);
|
||||
loopModal.value = true;
|
||||
@@ -959,7 +959,7 @@ const setTimeChartData = (chartData, xMax, xMin, yMax, yMin) => {
|
||||
};
|
||||
|
||||
// watch
|
||||
watch(conformanceTempReportData, (newValue) => {
|
||||
watch(allConformanceTempReportData, (newValue) => {
|
||||
if (newValue?.rule && newValue.rule.min !== null) {
|
||||
selectDurationTime.value = {
|
||||
min: newValue.rule.min,
|
||||
|
||||
@@ -180,7 +180,7 @@ const {
|
||||
selectedProcessScope,
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceAllTasks,
|
||||
allConformanceTask,
|
||||
cfmPtEteWhole,
|
||||
cfmPtEteStart: cfmPtEteStartStore,
|
||||
cfmPtEteEnd: cfmPtEteEndStore,
|
||||
@@ -204,7 +204,7 @@ const {
|
||||
conformanceRuleData,
|
||||
conformanceLogCreateCheckId,
|
||||
conformanceFilterCreateCheckId,
|
||||
conformanceTempReportData,
|
||||
allConformanceTempReportData,
|
||||
} = 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(conformanceTempReportData, (newValue) => {
|
||||
watch(allConformanceTempReportData, (newValue) => {
|
||||
if (reportWatchTimerId) clearTimeout(reportWatchTimerId);
|
||||
reportWatchTimerId = setTimeout(() => {
|
||||
if (newValue !== null) {
|
||||
|
||||
@@ -376,8 +376,8 @@ const {
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceTask,
|
||||
cfmSeqStart,
|
||||
cfmSeqEnd,
|
||||
allCfmSeqStart,
|
||||
allCfmSeqEnd,
|
||||
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(cfmSeqEnd.value, "sources", selectedTask)
|
||||
: cfmSeqStart.value.map((i) => i.label);
|
||||
? setSeqStartAndEndData(allCfmSeqEnd.value, "sources", selectedTask)
|
||||
: allCfmSeqStart.value.map((i) => i.label);
|
||||
});
|
||||
const cfmSeqEndData = computed(() => {
|
||||
const selectedTask = task.value ?? props.isSubmitShowDataSeq?.task ?? null;
|
||||
return isStartSelected.value
|
||||
? setSeqStartAndEndData(cfmSeqStart.value, "sinks", selectedTask)
|
||||
: cfmSeqEnd.value.map((i) => i.label);
|
||||
? setSeqStartAndEndData(allCfmSeqStart.value, "sinks", selectedTask)
|
||||
: allCfmSeqEnd.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 (cfmSeqStart, cfmSeqEnd, cfmPtEteSE, etc.).
|
||||
* @param {object} data - Activities list data from the backend (allCfmSeqStart, allCfmSeqEnd, 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 (cfmSeqStart or cfmSeqEnd).
|
||||
* @param {object} data - Activities list data from the backend (allCfmSeqStart or allCfmSeqEnd).
|
||||
* @param {string} category - 'sources' or 'sinks'.
|
||||
* @param {string} task - The selected activity task.
|
||||
* @returns {array}
|
||||
|
||||
@@ -250,10 +250,10 @@ const {
|
||||
selectedProcessScope,
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceAllTasks,
|
||||
allConformanceTask,
|
||||
conformanceTask,
|
||||
cfmSeqStart,
|
||||
cfmSeqEnd,
|
||||
allCfmSeqStart,
|
||||
allCfmSeqEnd,
|
||||
cfmPtEteStart,
|
||||
cfmPtEteEnd,
|
||||
cfmPtEteSE,
|
||||
@@ -471,7 +471,7 @@ emitter.on("actRadioData", (data) => {
|
||||
// Activity duration
|
||||
case "cfmDur":
|
||||
state.timeDuration = getDurationTime(
|
||||
conformanceAllTasks.value,
|
||||
allConformanceTask.value,
|
||||
"act",
|
||||
task,
|
||||
);
|
||||
|
||||
@@ -83,7 +83,7 @@ import getMoment from "moment";
|
||||
const props = defineProps(["selectValue"]);
|
||||
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
|
||||
const { allFilterTimeframe, 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(filterTimeframe.value.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(filterTimeframe.value.x_axis.max).getTime();
|
||||
const xAxisMin = new Date(allFilterTimeframe.value.x_axis.min).getTime();
|
||||
const xAxisMax = new Date(allFilterTimeframe.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 (!filterTimeframe.value?.data || filterTimeframe.value.data.length < 10)
|
||||
if (!allFilterTimeframe.value?.data || allFilterTimeframe.value.data.length < 10)
|
||||
return [];
|
||||
const data = filterTimeframe.value.data.map((i) => ({ x: i.x, y: i.y }));
|
||||
const data = allFilterTimeframe.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 = filterTimeframe.value.data[0].y;
|
||||
const d = allFilterTimeframe.value.data[0].y;
|
||||
const e = 2;
|
||||
const f = filterTimeframe.value.data[1].y;
|
||||
const f = allFilterTimeframe.value.data[1].y;
|
||||
b = (e * d - a * d - f * a - f * c) / (e - c - a);
|
||||
if (b < 0) {
|
||||
b = 0;
|
||||
}
|
||||
// Y-axis maximum value
|
||||
const ma = 9;
|
||||
const mb = filterTimeframe.value.data[8].y;
|
||||
const mb = allFilterTimeframe.value.data[8].y;
|
||||
const mc = 10;
|
||||
const md = filterTimeframe.value.data[9].y;
|
||||
const md = allFilterTimeframe.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: filterTimeframe.value.x_axis.min_base,
|
||||
x: allFilterTimeframe.value.x_axis.min_base,
|
||||
y: b,
|
||||
});
|
||||
// Add the maximum value
|
||||
data.push({
|
||||
x: filterTimeframe.value.x_axis.max_base,
|
||||
x: allFilterTimeframe.value.x_axis.max_base,
|
||||
y: mf,
|
||||
});
|
||||
|
||||
@@ -181,8 +181,8 @@ const timeFrameData = computed(() => {
|
||||
});
|
||||
|
||||
const labelsData = computed(() => {
|
||||
const min = new Date(filterTimeframe.value.x_axis.min_base).getTime();
|
||||
const max = new Date(filterTimeframe.value.x_axis.max_base).getTime();
|
||||
const min = new Date(allFilterTimeframe.value.x_axis.min_base).getTime();
|
||||
const max = new Date(allFilterTimeframe.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(filterTimeframe.value.x_axis.min);
|
||||
endTime.value = new Date(filterTimeframe.value.x_axis.max);
|
||||
startTime.value = new Date(allFilterTimeframe.value.x_axis.min);
|
||||
endTime.value = new Date(allFilterTimeframe.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 = filterTimeframe.value.y_axis.max * 1.1;
|
||||
const max = allFilterTimeframe.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(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);
|
||||
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);
|
||||
// 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,
|
||||
baseTraceTaskSeq,
|
||||
baseCases,
|
||||
allBaseTraceTaskSeq,
|
||||
allBaseCase,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
|
||||
@@ -240,7 +240,7 @@ const caseData = computed(() => {
|
||||
});
|
||||
|
||||
const columnData = computed(() => {
|
||||
const data = cloneDeep(baseCases.value);
|
||||
const data = cloneDeep(allBaseCase.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
|
||||
baseTraceTaskSeq.value.forEach((node, index) => {
|
||||
allBaseTraceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
@@ -398,7 +398,7 @@ function setNodesData() {
|
||||
*/
|
||||
function setEdgesData() {
|
||||
processMap.value.edges = [];
|
||||
baseTraceTaskSeq.value.forEach((edge, index) => {
|
||||
allBaseTraceTaskSeq.value.forEach((edge, index) => {
|
||||
processMap.value.edges.push({
|
||||
data: {
|
||||
source: `${index}`,
|
||||
@@ -431,7 +431,7 @@ function createCy() {
|
||||
function handleScroll(event) {
|
||||
if (
|
||||
infinitMaxItems.value ||
|
||||
baseCases.value.length < 20 ||
|
||||
allBaseCase.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, ...baseCases.value];
|
||||
infiniteData.value = [...infiniteData.value, ...allBaseCase.value];
|
||||
infiniteFinish.value = true;
|
||||
isLoading.value = false;
|
||||
} catch (error) {
|
||||
|
||||
@@ -128,7 +128,7 @@ const {
|
||||
infiniteStart,
|
||||
traceId,
|
||||
traces,
|
||||
traceTaskSeq,
|
||||
allTraceTaskSeq,
|
||||
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
|
||||
traceTaskSeq.value.forEach((node, index) => {
|
||||
allTraceTaskSeq.value.forEach((node, index) => {
|
||||
processMap.value.nodes.push({
|
||||
data: {
|
||||
id: index,
|
||||
@@ -281,7 +281,7 @@ function setNodesData() {
|
||||
*/
|
||||
function setEdgesData() {
|
||||
processMap.value.edges = [];
|
||||
traceTaskSeq.value.forEach((edge, index) => {
|
||||
allTraceTaskSeq.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, stats, createFilterId } = storeToRefs(allMapDataStore);
|
||||
const { logId, allStats, 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 (!stats.value) return;
|
||||
if (!allStats.value) return;
|
||||
statData.value = {
|
||||
cases: {
|
||||
count: stats.value.cases.count.toLocaleString("en-US"),
|
||||
total: stats.value.cases.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.cases.ratio),
|
||||
count: allStats.value.cases.count.toLocaleString("en-US"),
|
||||
total: allStats.value.cases.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.cases.ratio),
|
||||
},
|
||||
traces: {
|
||||
count: stats.value.traces.count.toLocaleString("en-US"),
|
||||
total: stats.value.traces.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.traces.ratio),
|
||||
count: allStats.value.traces.count.toLocaleString("en-US"),
|
||||
total: allStats.value.traces.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.traces.ratio),
|
||||
},
|
||||
task_instances: {
|
||||
count: stats.value.task_instances.count.toLocaleString("en-US"),
|
||||
total: stats.value.task_instances.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.task_instances.ratio),
|
||||
count: allStats.value.task_instances.count.toLocaleString("en-US"),
|
||||
total: allStats.value.task_instances.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.task_instances.ratio),
|
||||
},
|
||||
tasks: {
|
||||
count: stats.value.tasks.count.toLocaleString("en-US"),
|
||||
total: stats.value.tasks.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(stats.value.tasks.ratio),
|
||||
count: allStats.value.tasks.count.toLocaleString("en-US"),
|
||||
total: allStats.value.tasks.total.toLocaleString("en-US"),
|
||||
ratio: getPercentLabel(allStats.value.tasks.ratio),
|
||||
},
|
||||
started_at: getMoment(stats.value.started_at).format("YYYY-MM-DD HH:mm"),
|
||||
completed_at: getMoment(stats.value.completed_at).format(
|
||||
started_at: getMoment(allStats.value.started_at).format("YYYY-MM-DD HH:mm"),
|
||||
completed_at: getMoment(allStats.value.completed_at).format(
|
||||
"YYYY-MM-DD HH:mm",
|
||||
),
|
||||
case_duration: {
|
||||
min: getTimeLabel(stats.value.case_duration.min),
|
||||
max: getTimeLabel(stats.value.case_duration.max),
|
||||
average: getTimeLabel(stats.value.case_duration.average),
|
||||
median: getTimeLabel(stats.value.case_duration.median),
|
||||
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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+1
-3
@@ -20,7 +20,6 @@ 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";
|
||||
@@ -67,7 +66,6 @@ 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",
|
||||
@@ -107,7 +105,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 Cypress can add per-test overrides
|
||||
// Expose on window so Playwright can add per-test overrides
|
||||
window.__mswWorker__ = worker;
|
||||
window.__msw__ = { http, HttpResponse };
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
// 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.
|
||||
@@ -15,27 +17,7 @@
|
||||
* or "0" if totalSeconds is zero.
|
||||
*/
|
||||
export default function abbreviateNumber(totalSeconds) {
|
||||
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;
|
||||
const parsed = Number.parseInt(totalSeconds);
|
||||
if (parsed === 0) return "0";
|
||||
return formatDuration(parsed, { separator: " " });
|
||||
}
|
||||
|
||||
+134
-160
@@ -29,20 +29,23 @@ 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 to save a new filter with a user-provided name.
|
||||
* Shows a modal dialog prompting for a name and saves through the
|
||||
* given backend API function.
|
||||
*
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
export async function saveFilter(addFilterId, next = null) {
|
||||
async function promptSaveName(title, inputPlaceholder, save, onCancel = null) {
|
||||
let fileName = "";
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
|
||||
const { value, isConfirmed } = await Swal.fire({
|
||||
title: "SAVE NEW FILTER",
|
||||
title: title,
|
||||
input: "text",
|
||||
inputPlaceholder: "Enter Filter Name.",
|
||||
inputPlaceholder: inputPlaceholder,
|
||||
inputValue: fileName,
|
||||
inputAttributes: {
|
||||
maxlength: 200,
|
||||
@@ -64,7 +67,7 @@ export async function saveFilter(addFilterId, next = null) {
|
||||
// Determine whether to redirect based on the return value
|
||||
if (isConfirmed) {
|
||||
// Save succeeded
|
||||
await addFilterId(fileName);
|
||||
await save(fileName);
|
||||
// Show save complete notification
|
||||
if (value) {
|
||||
// Example of value: yes
|
||||
@@ -73,16 +76,34 @@ export async function saveFilter(addFilterId, next = 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.
|
||||
@@ -117,9 +138,67 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
|
||||
const allMapDataStore = useAllMapDataStore();
|
||||
const pageAdminStore = usePageAdminStore();
|
||||
|
||||
const result = await Swal.fire({
|
||||
await confirmLeave({
|
||||
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,
|
||||
@@ -132,31 +211,16 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
if (allMapDataStore.createFilterId) {
|
||||
await allMapDataStore.updateFilter();
|
||||
if (allMapDataStore.isUpdateFilter) {
|
||||
await savedSuccessfully(allMapDataStore.filterName);
|
||||
}
|
||||
} else {
|
||||
// Dangerous, here shows a modal
|
||||
await saveFilter(addFilterId, next);
|
||||
await saveChanges();
|
||||
if (navigateAfterSave) {
|
||||
logOut ? logOut() : next(toPath);
|
||||
}
|
||||
|
||||
logOut ? logOut() : next(toPath);
|
||||
} else if (result.dismiss === "cancel") {
|
||||
// console.log('popup cancel case', );
|
||||
// Handle page admin issue
|
||||
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
|
||||
pageAdminStore.keepPreviousPage();
|
||||
|
||||
allMapDataStore.tempFilterId = null;
|
||||
if (beforeDismiss) beforeDismiss();
|
||||
discardTemp();
|
||||
logOut ? logOut() : next(toPath);
|
||||
} else if (result.dismiss === "backdrop") {
|
||||
// console.log('popup backdrop case', );
|
||||
// Handle page admin issue
|
||||
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
|
||||
pageAdminStore.keepPreviousPage();
|
||||
|
||||
if (beforeDismiss) beforeDismiss();
|
||||
if (!logOut) {
|
||||
next(false);
|
||||
}
|
||||
@@ -171,47 +235,16 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
|
||||
* @returns {Promise<boolean>} True if the rule was saved, false otherwise.
|
||||
*/
|
||||
export async function saveConformance(addConformanceCreateCheckId) {
|
||||
let fileName = "";
|
||||
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;
|
||||
}
|
||||
return promptSaveName(
|
||||
"SAVE NEW RULE",
|
||||
"Enter Rule Name.",
|
||||
addConformanceCreateCheckId,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Prompts the user to save unsaved conformance rule changes before
|
||||
* leaving the Conformance page. Delegates to helper functions for
|
||||
* confirm, cancel, and backdrop scenarios.
|
||||
* leaving the Conformance page. Handles confirm (save), cancel
|
||||
* (discard), and backdrop (stay) scenarios.
|
||||
*
|
||||
* @param {Function} next - Vue Router next() guard callback.
|
||||
* @param {Function} addConformanceCreateCheckId - Backend API function
|
||||
@@ -227,91 +260,32 @@ export async function leaveConformance(
|
||||
logOut,
|
||||
) {
|
||||
const conformanceStore = useConformanceStore();
|
||||
const result = await showConfirmationDialog();
|
||||
|
||||
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({
|
||||
await confirmLeave({
|
||||
title: "SAVE YOUR RULE?",
|
||||
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);
|
||||
saveChanges: async () => {
|
||||
if (
|
||||
conformanceStore.conformanceFilterCreateCheckId ||
|
||||
conformanceStore.conformanceLogCreateCheckId
|
||||
) {
|
||||
await conformanceStore.updateConformance();
|
||||
if (conformanceStore.isUpdateConformance) {
|
||||
await savedSuccessfully(conformanceStore.conformanceFileName);
|
||||
}
|
||||
} else {
|
||||
await saveConformance(addConformanceCreateCheckId);
|
||||
}
|
||||
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;
|
||||
},
|
||||
navigateAfterSave: false,
|
||||
beforeDismiss: null,
|
||||
discardTemp: () => {
|
||||
conformanceStore.conformanceFilterTempCheckId = null;
|
||||
conformanceStore.conformanceLogTempCheckId = null;
|
||||
},
|
||||
next: next,
|
||||
toPath: toPath,
|
||||
logOut: logOut,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,10 +14,9 @@ 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(error, toastMessage) {
|
||||
export default function apiError(toastMessage) {
|
||||
const $toast = useToast();
|
||||
$toast.default(toastMessage, { position: "bottom" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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,30 +6,18 @@
|
||||
// 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.
|
||||
* 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.
|
||||
*
|
||||
* @param {number} num - The number to format.
|
||||
* @returns {string} The formatted number string (e.g. "1,234.56").
|
||||
*/
|
||||
export default function numberLabel(num) {
|
||||
let parts = num.toString().split(".");
|
||||
parts[0] = formatNumberWithCommas(parts[0]);
|
||||
const parts = num.toString().split(".");
|
||||
parts[0] = parts[0].replace(/(.)(?=(?:.{3})+$)/g, "$1,");
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
+22
-37
@@ -8,6 +8,8 @@
|
||||
|
||||
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
|
||||
@@ -241,26 +243,8 @@ 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.
|
||||
@@ -269,22 +253,23 @@ export function formatTime(seconds) {
|
||||
* @returns {Array<string>} Array of truncated strings (e.g. "2d 3h").
|
||||
*/
|
||||
export function formatMaxTwo(times) {
|
||||
const formattedTimes = [];
|
||||
for (let time of times) {
|
||||
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
|
||||
let units = time.match(/\d{1,10}[dhms]/g) || [];
|
||||
let formattedTime = "";
|
||||
let count = 0;
|
||||
|
||||
// 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;
|
||||
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")),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,17 +16,7 @@
|
||||
* @returns {Array<string|number>} The sorted array (same reference).
|
||||
*/
|
||||
export function sortNumEngZhtw(data) {
|
||||
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" });
|
||||
});
|
||||
return data.sort(sortNumEngZhtwForFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+43
-113
@@ -8,6 +8,8 @@
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import { pickDurationUnit } from "./formatDuration.js";
|
||||
|
||||
/** @constant {number} Number of decimal places for formatted time values. */
|
||||
const TOFIXED_DECIMAL = 1;
|
||||
|
||||
@@ -36,34 +38,11 @@ export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
|
||||
* character ("d", "h", "m", or "s") and the converted time value.
|
||||
*/
|
||||
const getTimeUnitAndValueToUse = (secondToDecide) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
const unit = pickDurationUnit(secondToDecide, true);
|
||||
return {
|
||||
unitToUse: unit.symbol,
|
||||
timeValue: secondToDecide / unit.seconds,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -80,6 +59,27 @@ 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,27 +89,12 @@ export function getYTicksByIndex(stepSize, index, unitToUse) {
|
||||
* "30 mins", "5 sec").
|
||||
*/
|
||||
export function getTimeLabel(second, fixedNumber = 0) {
|
||||
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";
|
||||
return scaledTimeLabel(second, fixedNumber, {
|
||||
d: " days",
|
||||
h: " hrs",
|
||||
m: " mins",
|
||||
s: " sec",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,24 +108,12 @@ export function getTimeLabel(second, fixedNumber = 0) {
|
||||
* @returns {string} The formatted string (e.g. "1d", "6.8h", "30m", "5s").
|
||||
*/
|
||||
export function simpleTimeLabel(second, fixedNumber = 0) {
|
||||
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";
|
||||
return scaledTimeLabel(second, fixedNumber, {
|
||||
d: "d",
|
||||
h: "h",
|
||||
m: "m",
|
||||
s: "s",
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Converts seconds to a time string using the same unit as the maximum
|
||||
@@ -155,52 +128,9 @@ 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 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;
|
||||
const unit = pickDurationUnit(max, true);
|
||||
const value = second / unit.seconds;
|
||||
return value.toFixed(value === 0 ? 0 : fixedNumber) + unit.symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(error, 'Failed to get all users.');
|
||||
apiError('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(error, 'Failed to add a new account.');
|
||||
apiError('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(error, 'Failed to delete the account.');
|
||||
apiError('Failed to delete the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -191,7 +191,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError(error, 'Failed to edit the account.');
|
||||
apiError('Failed to edit the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -207,7 +207,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError(error, 'Failed to edit name of account.');
|
||||
apiError('Failed to edit name of account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -224,7 +224,7 @@ export const useAcctMgmtStore = defineStore('acctMgmtStore', {
|
||||
});
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
apiError(error, 'Failed to edit password of account.');
|
||||
apiError('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(error, 'Failed to add role to the account.');
|
||||
apiError('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(error, 'Failed to delete a role from the account.');
|
||||
apiError('Failed to delete a role from the account.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
+10
-52
@@ -71,57 +71,18 @@ 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) => {
|
||||
@@ -155,9 +116,6 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
}
|
||||
return [];
|
||||
},
|
||||
allFunnels: (state) => {
|
||||
return state.allFunnelData;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
@@ -172,7 +130,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.allStats = response.data.stats;
|
||||
this.allInsights = response.data.insights;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Map.");
|
||||
apiError("Failed to load the Map.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -191,7 +149,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.allBaseTrace = baseResponse.data;
|
||||
}
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Trace.");
|
||||
apiError("Failed to load the Trace.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -233,7 +191,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.infinite404 = 404;
|
||||
return;
|
||||
}
|
||||
apiError(error, "Failed to load the Trace Detail.");
|
||||
apiError("Failed to load the Trace Detail.");
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -278,7 +236,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.infinite404 = 404;
|
||||
return;
|
||||
}
|
||||
apiError(error, "Failed to load the Base Trace Detail.");
|
||||
apiError("Failed to load the Base Trace Detail.");
|
||||
return [];
|
||||
}
|
||||
},
|
||||
@@ -309,7 +267,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(error, "Failed to load the Filter Parameters.");
|
||||
apiError("Failed to load the Filter Parameters.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -323,7 +281,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
const response = await apiClient.post(api, this.postRuleData);
|
||||
this.hasResultRule = response.data.result;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Has Result.");
|
||||
apiError("Failed to load the Has Result.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -337,7 +295,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
const response = await apiClient.post(api, this.postRuleData);
|
||||
this.tempFilterId = response.data.id;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to add the Temporary Filters.");
|
||||
apiError("Failed to add the Temporary Filters.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -357,7 +315,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.createFilterId = response.data.id;
|
||||
this.tempFilterId = null;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Filters.");
|
||||
apiError("Failed to load the Filters.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -375,7 +333,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.filterName = response.data.name;
|
||||
this.baseLogId = response.data.log?.id;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to get Filter Detail.");
|
||||
apiError("Failed to get Filter Detail.");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -392,7 +350,7 @@ export const useAllMapDataStore = defineStore("allMapDataStore", {
|
||||
this.isUpdateFilter = response.status === 200;
|
||||
this.tempFilterId = null;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to update an Existing Filter.");
|
||||
apiError("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(error, "Failed to load the Compare.");
|
||||
apiError("Failed to load the Compare.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -63,7 +63,7 @@ export const useCompareStore = defineStore("compareStore", {
|
||||
|
||||
return response.data.stats;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Compare's States.");
|
||||
apiError("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(error, "Failed to load the Compare's file name.");
|
||||
apiError("Failed to load the Compare's file name.");
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
@@ -91,18 +91,9 @@ 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;
|
||||
},
|
||||
@@ -157,18 +148,6 @@ 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) => {
|
||||
@@ -220,12 +199,6 @@ 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) => {
|
||||
@@ -276,7 +249,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.allWaitingTime = response.data.waiting_time;
|
||||
this.allCycleTime = response.data.cycle_time;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Conformance Parameters.");
|
||||
apiError("Failed to load the Conformance Parameters.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -295,7 +268,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.conformanceLogTempCheckId = response.data.id;
|
||||
}
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to add the Temporary Check for a file.");
|
||||
apiError("Failed to add the Temporary Check for a file.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -312,7 +285,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.allConformanceTempReportData = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to Get the Temporary Log Conformance Report.");
|
||||
apiError("Failed to Get the Temporary Log Conformance Report.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -326,7 +299,6 @@ 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.",
|
||||
);
|
||||
}
|
||||
@@ -350,7 +322,6 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
return null;
|
||||
}
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance issue.",
|
||||
);
|
||||
return null;
|
||||
@@ -367,7 +338,6 @@ 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.",
|
||||
);
|
||||
}
|
||||
@@ -391,7 +361,6 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
return null;
|
||||
}
|
||||
apiError(
|
||||
error,
|
||||
"Failed to Get the detail of a temporary log conformance loop.",
|
||||
);
|
||||
return null;
|
||||
@@ -419,7 +388,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.conformanceLogTempCheckId = null;
|
||||
}
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to add the Conformance Check for a file.");
|
||||
apiError("Failed to add the Conformance Check for a file.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -435,7 +404,7 @@ export const useConformanceStore = defineStore("conformanceStore", {
|
||||
this.conformanceLogTempCheckId = null;
|
||||
this.conformanceFilterTempCheckId = null;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to update an Existing Conformance.");
|
||||
apiError("Failed to update an Existing Conformance.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -26,8 +26,8 @@ export interface Node {
|
||||
}
|
||||
|
||||
export interface NodePositions {
|
||||
[direction: string]: {
|
||||
[graphId: string]: Node[];
|
||||
[graphId: string]: {
|
||||
[direction: string]: Node[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,16 +51,10 @@ export const useCytoscapeStore = defineStore('cytoscapeStore', {
|
||||
if (!this.nodePositions[this.currentGraphId][direction]) {
|
||||
this.nodePositions[this.currentGraphId][direction] = [];
|
||||
}
|
||||
// If this graph's data was previously stored in localStorage
|
||||
if (localStorage.getItem(SAVE_KEY_NAME)) {
|
||||
const nodeToSave = this.nodePositions[this.currentGraphId][direction]
|
||||
.find(node => node.id === nodeId);
|
||||
if (nodeToSave) {
|
||||
nodeToSave.position = position;
|
||||
} else {
|
||||
this.nodePositions[this.currentGraphId][direction]
|
||||
.push({ id: nodeId, position: position });
|
||||
}
|
||||
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 });
|
||||
|
||||
+9
-9
@@ -146,7 +146,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
: null;
|
||||
});
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the files.");
|
||||
apiError("Failed to load the files.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -180,7 +180,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
}
|
||||
} else {
|
||||
Swal.close(); // Close the loading progress bar
|
||||
apiError(error, "Failed to upload the files.");
|
||||
apiError("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(error, "Failed to get upload detail.");
|
||||
apiError("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(error, "Failed to upload the log files.");
|
||||
apiError("Failed to upload the log files.");
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -247,7 +247,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
this.uploadFileName = null;
|
||||
await this.fetchAllFiles();
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to rename.");
|
||||
apiError("Failed to rename.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -262,7 +262,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
);
|
||||
this.allDependentsData = response.data;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to get Dependents of the files.");
|
||||
apiError("Failed to get Dependents of the files.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -282,7 +282,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
await this.fetchAllFiles();
|
||||
await deleteSuccess();
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to delete.");
|
||||
apiError("Failed to delete.");
|
||||
} finally {
|
||||
loading.isLoading = false;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ export const useFilesStore = defineStore("filesStore", {
|
||||
try {
|
||||
await apiClient.delete(api);
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to Remove a Deletion Record.");
|
||||
apiError("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(error, "Failed to download.");
|
||||
apiError("Failed to download.");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ export const useLoginStore = defineStore("loginStore", {
|
||||
|
||||
this.userData = response.data;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load user data.");
|
||||
apiError("Failed to load user data.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
+1
-3
@@ -5,13 +5,11 @@
|
||||
// imacat.yang@dsp.im (imacat), 2023/9/23
|
||||
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
||||
/**
|
||||
* @module stores/main Pinia store instance with persisted state plugin.
|
||||
* @module stores/main Pinia store instance.
|
||||
*/
|
||||
|
||||
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 { insights } = useAllMapDataStore();
|
||||
this.insights = { ...insights };
|
||||
const { allInsights } = useAllMapDataStore();
|
||||
this.insights = { ...allInsights };
|
||||
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 { insights } = useAllMapDataStore();
|
||||
this.insights = { ...insights };
|
||||
const { allInsights } = useAllMapDataStore();
|
||||
this.insights = { ...allInsights };
|
||||
this.matchGraphPathWithInsightsPath();
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ export const usePerformanceStore = defineStore("performanceStore", {
|
||||
const response = await apiClient.get(api);
|
||||
this.allPerformanceData = response.data;
|
||||
} catch (error) {
|
||||
apiError(error, "Failed to load the Performance.");
|
||||
apiError("Failed to load the Performance.");
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// 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));
|
||||
};
|
||||
@@ -86,23 +86,23 @@
|
||||
></SidebarView>
|
||||
<SidebarState
|
||||
v-model:visible="sidebarState"
|
||||
:insights="insights"
|
||||
:stats="stats"
|
||||
:insights="allInsights"
|
||||
:stats="allStats"
|
||||
></SidebarState>
|
||||
<SidebarTraces
|
||||
v-model:visible="sidebarTraces"
|
||||
:cases="cases"
|
||||
:cases="allCase"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="tracesViewRef"
|
||||
>
|
||||
</SidebarTraces>
|
||||
<SidebarFilter
|
||||
v-model:visible="sidebarFilter"
|
||||
:filterTasks="filterTasks"
|
||||
:filterStartToEnd="filterStartToEnd"
|
||||
:filterEndToStart="filterEndToStart"
|
||||
:filterTimeframe="filterTimeframe"
|
||||
:filterTrace="filterTrace"
|
||||
:filterTasks="allFilterTask"
|
||||
:filterStartToEnd="allFilterStartToEnd"
|
||||
:filterEndToStart="allFilterEndToStart"
|
||||
:filterTimeframe="allFilterTimeframe"
|
||||
:filterTrace="allFilterTrace"
|
||||
@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.routeFile;
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -176,25 +176,25 @@ const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const {
|
||||
processMap,
|
||||
bpmn,
|
||||
stats,
|
||||
insights,
|
||||
allBpmn,
|
||||
allStats,
|
||||
allInsights,
|
||||
traceId,
|
||||
traces,
|
||||
baseTraces,
|
||||
baseTraceId,
|
||||
filterTasks,
|
||||
filterStartToEnd,
|
||||
filterEndToStart,
|
||||
filterTimeframe,
|
||||
filterTrace,
|
||||
allFilterTask,
|
||||
allFilterStartToEnd,
|
||||
allFilterEndToStart,
|
||||
allFilterTimeframe,
|
||||
allFilterTrace,
|
||||
temporaryData,
|
||||
isRuleData,
|
||||
ruleData,
|
||||
logId,
|
||||
baseLogId,
|
||||
createFilterId,
|
||||
cases,
|
||||
allCase,
|
||||
postRuleData,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
@@ -401,7 +401,7 @@ function setNodesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.nodes = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.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 : bpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.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 : bpmn.value;
|
||||
const mapSource = type === "processMap" ? processMap.value : allBpmn.value;
|
||||
|
||||
if (mapSource.vertices.length !== 0) {
|
||||
setNodesData(mapData);
|
||||
|
||||
@@ -42,7 +42,7 @@ defineOptions({
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport();
|
||||
to.meta.file = await conformanceStore.conformanceTempReportData?.file;
|
||||
to.meta.file = await conformanceStore.allConformanceTempReportData?.file;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -68,7 +68,7 @@ const {
|
||||
selectedActSeqMore,
|
||||
selectedActSeqFromTo,
|
||||
conformanceRuleData,
|
||||
conformanceTempReportData,
|
||||
allConformanceTempReportData,
|
||||
conformanceFileName,
|
||||
} = storeToRefs(conformanceStore);
|
||||
|
||||
|
||||
@@ -86,22 +86,22 @@
|
||||
></SidebarView>
|
||||
<SidebarState
|
||||
v-model:visible="sidebarState"
|
||||
:insights="insights"
|
||||
:stats="stats"
|
||||
:insights="allInsights"
|
||||
:stats="allStats"
|
||||
></SidebarState>
|
||||
<SidebarTraces
|
||||
v-model:visible="sidebarTraces"
|
||||
:cases="cases"
|
||||
:cases="allCase"
|
||||
@switch-Trace-Id="switchTraceId"
|
||||
ref="tracesViewRef"
|
||||
></SidebarTraces>
|
||||
<SidebarFilter
|
||||
v-model:visible="sidebarFilter"
|
||||
:filterTasks="filterTasks"
|
||||
:filterStartToEnd="filterStartToEnd"
|
||||
:filterEndToStart="filterEndToStart"
|
||||
:filterTimeframe="filterTimeframe"
|
||||
:filterTrace="filterTrace"
|
||||
:filterTasks="allFilterTask"
|
||||
:filterStartToEnd="allFilterStartToEnd"
|
||||
:filterEndToStart="allFilterEndToStart"
|
||||
:filterTimeframe="allFilterTimeframe"
|
||||
:filterTrace="allFilterTrace"
|
||||
@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.routeFile;
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
@@ -175,25 +175,25 @@ const allMapDataStore = useAllMapDataStore();
|
||||
const { isLoading } = storeToRefs(loadingStore);
|
||||
const {
|
||||
processMap,
|
||||
bpmn,
|
||||
stats,
|
||||
insights,
|
||||
allBpmn,
|
||||
allStats,
|
||||
allInsights,
|
||||
traceId,
|
||||
traces,
|
||||
baseTraces,
|
||||
baseTraceId,
|
||||
filterTasks,
|
||||
filterStartToEnd,
|
||||
filterEndToStart,
|
||||
filterTimeframe,
|
||||
filterTrace,
|
||||
allFilterTask,
|
||||
allFilterStartToEnd,
|
||||
allFilterEndToStart,
|
||||
allFilterTimeframe,
|
||||
allFilterTrace,
|
||||
temporaryData,
|
||||
isRuleData,
|
||||
ruleData,
|
||||
logId,
|
||||
baseLogId,
|
||||
createFilterId,
|
||||
cases,
|
||||
allCase,
|
||||
postRuleData,
|
||||
} = storeToRefs(allMapDataStore);
|
||||
|
||||
@@ -398,7 +398,7 @@ function setNodesData(mapData) {
|
||||
};
|
||||
|
||||
mapData.nodes = [];
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : bpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.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 : bpmn.value;
|
||||
const mapSource = mapTypeVal === "processMap" ? processMap.value : allBpmn.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 : bpmn.value;
|
||||
const mapSource = type === "processMap" ? processMap.value : allBpmn.value;
|
||||
|
||||
if (mapSource.vertices.length !== 0) {
|
||||
setNodesData(mapData);
|
||||
|
||||
@@ -263,7 +263,7 @@ export default {
|
||||
break;
|
||||
}
|
||||
await conformanceStore.getConformanceReport(true);
|
||||
to.meta.file = conformanceStore.routeFile;
|
||||
to.meta.file = conformanceStore.allRouteFile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
@@ -46,9 +46,7 @@ export default defineConfig(({ mode }) => {
|
||||
base: "/",
|
||||
resolve: {
|
||||
alias: {
|
||||
util: "util/",
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"/images": "src/assets/images",
|
||||
},
|
||||
},
|
||||
server: {
|
||||
@@ -61,13 +59,7 @@ 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