From 609a665b69b1d416e313fdef83ce591070a6eda3 Mon Sep 17 00:00:00 2001 From: Yash Maheshwari Date: Wed, 24 Jul 2024 18:10:07 +0530 Subject: [PATCH 1/2] Implemented: support to let user define the cron expression(#245) Added modal to define crone expression for schedule Displayed predefined options to select an expression Removed some cron expressions from env file --- .env.example | 2 +- package-lock.json | 9 +++ package.json | 1 + src/components/ScheduleModal.vue | 104 +++++++++++++++++++++++++++++++ src/locales/en.json | 3 + src/views/BrokeringRoute.vue | 72 +++++++++------------ 6 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 src/components/ScheduleModal.vue diff --git a/.env.example b/.env.example index 84a096f..381858a 100644 --- a/.env.example +++ b/.env.example @@ -8,5 +8,5 @@ VUE_APP_RULE_ENUMS={"QUEUE":{"id":"OIP_QUEUE","code":"facilityId"},"SHIPPING_MET VUE_APP_RULE_FILTER_ENUMS={"FACILITY_GROUP":{"id":"IIP_FACILITY_GROUP","code":"facilityGroupId"},"PROXIMITY":{"id":"IIP_PROXIMITY","code":"distance"},"BRK_SAFETY_STOCK":{"id":"IIP_BRK_SFTY_STOCK","code":"brokeringSafetyStock"},"MEASUREMENT_SYSTEM":{"id":"IIP_MSMNT_SYSTEM","code":"measurementSystem"},"SPLIT_ITEM_GROUP":{"id":"IIP_SPLIT_ITEM_GROUP","code":"splitOrderItemGroup"}} VUE_APP_RULE_SORT_ENUMS={"PROXIMITY":{"id":"ISP_PROXIMITY","code":"distance"},"INV_BALANCE":{"id":"ISP_INV_BAL","code":"inventoryForAllocation"},"CUSTOMER_SEQ":{"id":"ISP_CUST_SEQ","code":"facilitySequence"}} VUE_APP_RULE_ACTION_ENUMS={"RM_AUTO_CANCEL_DATE":{"id":"ORA_RM_CANCEL_DATE","code":"RM_AUTO_CANCEL_DATE"},"AUTO_CANCEL_DAYS":{"id":"ORA_AUTO_CANCEL_DAYS","code":"ADD_AUTO_CANCEL_DATE"},"NEXT_RULE":{"id":"ORA_NEXT_RULE","code":"NEXT_RULE"},"MOVE_TO_QUEUE":{"id":"ORA_MV_TO_QUEUE","code":"MOVE_TO_QUEUE"}} -VUE_APP_CRON_EXPRESSIONS={"Every 5 minutes":"0 */5 * ? * *","Every 15 minutes":"0 */15 * ? * *","Every 15 minutes(8am - 2pm)":"0 */15 8-14 ? * *","Every 30 minutes":"0 */30 * ? * *","Every 30 minutes(8am - 2pm)":"0 */30 8-14 ? * *","Hourly":"0 0 * ? * *","Every six hours":"0 0 */6 ? * *","Every day at midnight":"0 0 0 * * ?"} +VUE_APP_CRON_EXPRESSIONS={"Every 5 minutes":"0 */5 * ? * *","Every 15 minutes":"0 */15 * ? * *","Every 30 minutes":"0 */30 * ? * *","Hourly":"0 0 * ? * *","Every six hours":"0 0 */6 ? * *","Every day at midnight":"0 0 0 * * ?"} VUE_APP_LOGIN_URL="https://launchpad.hotwax.io/login" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 627f40c..fc3a55e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "axios": "^0.21.1", "axios-cache-adapter": "^2.7.3", "core-js": "^3.6.5", + "cronstrue": "^2.50.0", "http-status-codes": "^2.1.4", "luxon": "^2.3.0", "mitt": "^2.1.0", @@ -6693,6 +6694,14 @@ "node": ">=8" } }, + "node_modules/cronstrue": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", + "integrity": "sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==", + "bin": { + "cronstrue": "bin/cli.js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index 517fc52..ad569c1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "axios": "^0.21.1", "axios-cache-adapter": "^2.7.3", "core-js": "^3.6.5", + "cronstrue": "^2.50.0", "http-status-codes": "^2.1.4", "luxon": "^2.3.0", "mitt": "^2.1.0", diff --git a/src/components/ScheduleModal.vue b/src/components/ScheduleModal.vue new file mode 100644 index 0000000..518e0b9 --- /dev/null +++ b/src/components/ScheduleModal.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/locales/en.json b/src/locales/en.json index 34b51d6..bc6d094 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -45,6 +45,7 @@ "Enter a valid value": "Enter a valid value", "Error getting user profile": "Error getting user profile", "Execution history": "Execution history", + "Expression": "Expression", "Failed to clone brokering run": "Failed to clone brokering run", "Failed to clone rule": "Failed to clone rule", "Failed to clone the rule": "Failed to clone the rule", @@ -91,6 +92,7 @@ "New Run": "New Run", "New routing created": "New routing created", "Next rule": "Next rule", + "Next run": "Next run", "No available history for this group": "No available history for this group", "No available history for this route": "No available history for this route", "No archived routings": "No archived routings", @@ -139,6 +141,7 @@ "Save changes": "Save changes", "Save changes before moving to the details page of unarchived route": "Save changes before moving to the details page of unarchived route", "Schedule": "Schedule", + "Schedule Options": "Schedule Options", "Scheduler": "Scheduler", "Search time zones": "Search time zones", "Select": "Select", diff --git a/src/views/BrokeringRoute.vue b/src/views/BrokeringRoute.vue index 6b074b9..2853ef8 100644 --- a/src/views/BrokeringRoute.vue +++ b/src/views/BrokeringRoute.vue @@ -89,20 +89,21 @@ - - - {{ translate("Run time") }} - - {{ job.paused === 'N' ? getDateAndTime(job.nextExecutionDateTime) : "-" }} - - + - {{ translate("Schedule") }} + + {{ cronstrue.toString(job.cronExpression) || job.cronExpression }} + + + + {{ translate("Next run") }} + + {{ job.paused === 'N' ? getDateAndTime(job.nextExecutionDateTime) : "-" }} @@ -216,6 +217,8 @@ import emitter from "@/event-bus"; import { translate } from "@/i18n"; import GroupHistoryModal from "@/components/GroupHistoryModal.vue" import RoutingHistoryModal from "@/components/RoutingHistoryModal.vue" +import cronstrue from "cronstrue" +import ScheduleModal from "@/components/ScheduleModal.vue"; const router = useRouter(); const store = useStore(); @@ -226,7 +229,6 @@ const props = defineProps({ } }) -const cronExpressions = JSON.parse(process.env?.VUE_APP_CRON_EXPRESSIONS as string) let routingsForReorder = ref([]) let description = ref("") let isDescUpdating = ref(false) @@ -296,11 +298,6 @@ onBeforeRouteLeave(async (to) => { return; }) -function updateCronExpression(event: CustomEvent) { - job.value.cronExpression = event.detail.value - saveChanges() -} - function initializeOrderRoutings() { routingsForReorder.value = JSON.parse(JSON.stringify(getActiveAndDraftOrderRoutings())) } @@ -309,28 +306,6 @@ async function checkOmsConnectionStatus() { await store.dispatch("util/checkOmsConnectionStatus") } -async function saveChanges() { - const alert = await alertController - .create({ - header: translate("Save changes"), - message: translate("Are you sure you want to save these changes?"), - buttons: [{ - text: translate("Cancel"), - handler: () => { - // If clicking cancel reverting the value for cronExpression to original value, so that user does not gets confused that whether the value is changed or not - job.value.cronExpression = currentRoutingGroup.value["schedule"] ? JSON.parse(JSON.stringify(currentRoutingGroup.value))["schedule"].cronExpression : '' - }, - role: "cancel" - }, { - text: translate("Save"), - handler: async () => { - await saveSchedule() - } - }] - }); - return alert.present(); -} - async function fetchGroupHistory() { groupHistory.value = [] @@ -378,10 +353,9 @@ async function saveSchedule() { const resp = await OrderRoutingService.scheduleBrokering(payload) if(!hasError(resp)) { showToast(translate("Job updated")) - await store.dispatch("orderRouting/setCurrentGroup", JSON.parse(JSON.stringify({ - ...currentRoutingGroup.value, - schedule: job.value - }))) + // Fetching the group schedule information again after making changes to the job schedule to fetch the correct nextExecutionTime for job, doing so as we do not get the updated information in POST schedule api call + await store.dispatch("orderRouting/fetchCurrentGroupSchedule", { routingGroupId: props.routingGroupId, currentGroup: currentRoutingGroup.value }) + job.value = currentRoutingGroup.value["schedule"] ? JSON.parse(JSON.stringify(currentRoutingGroup.value))["schedule"] : {} } else { throw resp.data } @@ -651,6 +625,22 @@ async function openRoutingHistoryModal(orderRoutingId: string, routingName: stri routingHistoryModal.present(); } +async function openScheduleModal() { + const scheduleModal = await modalController.create({ + component: ScheduleModal, + componentProps: { cronExpression: job.value.cronExpression } + }) + + scheduleModal.onDidDismiss().then(async (result: any) => { + if(result?.data?.expression) { + job.value.cronExpression = result.data.expression + await saveSchedule() + } + }) + + scheduleModal.present(); +} + async function updateOrderRouting(routing: Route, fieldToUpdate: string, value: string) { orderRoutings.value.map((route: any) => { if(route.orderRoutingId === routing.orderRoutingId) { From 4f5353e8c574d200c30b79cc0ae11e4e6e773a8a Mon Sep 17 00:00:00 2001 From: Yash Maheshwari Date: Tue, 13 Aug 2024 16:33:06 +0530 Subject: [PATCH 2/2] Implemented: checks to validate cron expression before updating the schedule(#245) --- package-lock.json | 20 +++++++++++++ package.json | 1 + src/components/ScheduleModal.vue | 49 ++++++++++++++++++++++++++++---- src/views/BrokeringRoute.vue | 11 ++++++- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc3a55e..aeb742e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "axios": "^0.21.1", "axios-cache-adapter": "^2.7.3", "core-js": "^3.6.5", + "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", "http-status-codes": "^2.1.4", "luxon": "^2.3.0", @@ -6694,6 +6695,25 @@ "node": ">=8" } }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cron-parser/node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/cronstrue": { "version": "2.50.0", "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", diff --git a/package.json b/package.json index ad569c1..9c12b6b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "axios": "^0.21.1", "axios-cache-adapter": "^2.7.3", "core-js": "^3.6.5", + "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", "http-status-codes": "^2.1.4", "luxon": "^2.3.0", diff --git a/src/components/ScheduleModal.vue b/src/components/ScheduleModal.vue index 518e0b9..8f94c80 100644 --- a/src/components/ScheduleModal.vue +++ b/src/components/ScheduleModal.vue @@ -13,11 +13,15 @@ - + - {{ cronstrue.toString(expression) || "-" }} + {{ isExpressionValid && getCronString ? getCronString : "-" }} + + + + {{ isExpressionValid && getCronString ? getNextExecutionTime : "Provide a valid cron expression" }} @@ -31,7 +35,7 @@ - + @@ -60,9 +64,13 @@ import { IonToolbar, modalController, } from "@ionic/vue"; -import { closeOutline, saveOutline, timerOutline } from "ionicons/icons"; -import { defineProps, ref } from "vue"; +import { closeOutline, saveOutline, timeOutline, timerOutline } from "ionicons/icons"; +import { computed, defineProps, ref } from "vue"; import cronstrue from "cronstrue"; +import cronParser from "cron-parser"; +import logger from "@/logger"; +import { getDateAndTime } from "@/utils"; +import store from "@/store"; const props = defineProps({ cronExpression: { @@ -73,6 +81,36 @@ const props = defineProps({ let expression = ref(props.cronExpression) const cronExpressions = JSON.parse(process.env?.VUE_APP_CRON_EXPRESSIONS as string) +const userProfile = computed(() => store.getters["user/getUserProfile"]) + +const isExpressionValid = computed(() => { + try { + cronParser.parseExpression(expression.value, { tz: userProfile.value.timeZone }) + return true + } catch(e) { + logger.warn("Invalid expression", e) + return false + } +}) + +const getCronString = computed(() => { + try { + return cronstrue.toString(expression.value) + } catch(e) { + logger.warn(e) + return "" + } +}) + +const getNextExecutionTime = computed(() => { + try { + const interval = cronParser.parseExpression(expression.value, { tz: userProfile.value.timeZone }) + return getDateAndTime((interval.next() as any)["_date"].ts) + } catch(e) { + logger.error("Invalid expression", e) + return "" + } +}) // Not passing any data on modal close as we are updating the routings on every button click. function closeModal(expression = "") { @@ -80,7 +118,6 @@ function closeModal(expression = "") { } function isExpressionUpdated() { - console.log(props.cronExpression, expression.value) return props.cronExpression !== expression.value } diff --git a/src/views/BrokeringRoute.vue b/src/views/BrokeringRoute.vue index 2853ef8..f7c953e 100644 --- a/src/views/BrokeringRoute.vue +++ b/src/views/BrokeringRoute.vue @@ -97,7 +97,7 @@ {{ description }} --> - {{ cronstrue.toString(job.cronExpression) || job.cronExpression }} + {{ getCronString() || job.cronExpression }} @@ -298,6 +298,15 @@ onBeforeRouteLeave(async (to) => { return; }) +function getCronString() { + try { + return cronstrue.toString(job.value.cronExpression) + } catch(e) { + logger.error(e) + return "" + } +} + function initializeOrderRoutings() { routingsForReorder.value = JSON.parse(JSON.stringify(getActiveAndDraftOrderRoutings())) }