diff --git a/.env.example b/.env.example index 2b19d2f7..3dd51432 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,7 @@ VUE_APP_ALIAS= VUE_APP_DEFAULT_LOG_LEVEL="error" VUE_APP_LOGIN_URL="http://launchpad.hotwax.io/login" VUE_APP_BROKER_JOB_ENUMS = {"REJ_ORDR":"JOB_BKR_REJ_ORD"} +VUE_APP_GITBOOK_API_KEY="" +VUE_APP_SPACE_ID="" +VUE_APP_GITBOOK_BASE_URL="https://api.gitbook.com/v1" +VUE_APP_GITBOOK_JOBS_DOCS_URL="https://docs.hotwax.co/documents/retail-operations/workflow/job-workflows" diff --git a/package-lock.json b/package-lock.json index e6849f12..af464c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@hotwax/app-version-info": "^1.0.0", "@hotwax/apps-theme": "^1.2.6", "@hotwax/dxp-components": "1.15.4", - "@hotwax/oms-api": "1.14.0", + "@hotwax/oms-api": "1.15.0", "@ionic/core": "^7.6.0", "@ionic/vue": "^7.6.0", "@ionic/vue-router": "^7.6.0", @@ -2680,8 +2680,9 @@ } }, "node_modules/@hotwax/oms-api": { - "version": "1.14.0", - "license": "Apache-2.0", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@hotwax/oms-api/-/oms-api-1.15.0.tgz", + "integrity": "sha512-6WXJ5z5JaAxgKith6dblLchP471xUYcFaeggN5onWZBkgGx5NqkLT0rgLWTW/UcVQMokxKtzSc+mcJ4eI1mGww==", "dependencies": { "@types/node-json-transform": "^1.0.0", "axios": "^0.21.1", diff --git a/package.json b/package.json index 4b58abf3..6df3d9f4 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@hotwax/app-version-info": "^1.0.0", "@hotwax/apps-theme": "^1.2.6", "@hotwax/dxp-components": "1.15.4", - "@hotwax/oms-api": "1.14.0", + "@hotwax/oms-api": "1.15.0", "@ionic/core": "^7.6.0", "@ionic/vue": "^7.6.0", "@ionic/vue-router": "^7.6.0", diff --git a/src/adapter/index.ts b/src/adapter/index.ts index a31b0067..60edcc82 100644 --- a/src/adapter/index.ts +++ b/src/adapter/index.ts @@ -1,7 +1,8 @@ -import { api, client, getConfig, initialise, logout, resetConfig, updateInstanceUrl, updateToken, setUserTimeZone, getAvailableTimeZones} from '@hotwax/oms-api' +import { api, askQuery, client, getConfig, initialise, logout, resetConfig, searchQuery, updateInstanceUrl, updateToken, setUserTimeZone, getAvailableTimeZones} from '@hotwax/oms-api' export { api, + askQuery, client, getConfig, initialise, @@ -9,6 +10,7 @@ export { resetConfig, updateInstanceUrl, updateToken, + searchQuery, setUserTimeZone, getAvailableTimeZones } \ No newline at end of file diff --git a/src/components/JobConfiguration.vue b/src/components/JobConfiguration.vue index a786e546..177e57f1 100644 --- a/src/components/JobConfiguration.vue +++ b/src/components/JobConfiguration.vue @@ -14,7 +14,9 @@ <ion-item v-if="currentJob.description" lines="none"> <ion-label class="ion-text-wrap"> - <p>{{ currentJob.description }}</p> + <p>{{ currentJob.description }} + <ion-text class="learn-more-text" color="primary" @click="openLearnMoreModal()">{{ translate("Learn more") }}</ion-text> + </p> </ion-label> </ion-item> @@ -170,6 +172,7 @@ import { IonRow, IonSelect, IonSelectOption, + IonText, alertController, modalController, } from "@ionic/vue"; @@ -200,6 +203,7 @@ import emitter from '@/event-bus'; import { Actions, hasPermission } from '@/authorization' import CustomFrequencyModal from '@/components/CustomFrequencyModal.vue'; import JobParameterModal from '@/components/JobParameterModal.vue' +import LearnMoreModal from "@/components/LearnMoreModal.vue"; export default defineComponent({ name: "JobConfiguration", @@ -218,7 +222,8 @@ export default defineComponent({ IonRow, IonSelect, IonSelectOption, - IonCheckbox + IonCheckbox, + IonText }, data() { return { @@ -279,6 +284,13 @@ export default defineComponent({ const jobId = this.currentJob.jobId this.router.push({ name: 'DataManagerLogDetails', params: { jobId } }) }, + async openLearnMoreModal() { + const learnMoreModal = await modalController.create({ + component: LearnMoreModal, + componentProps: {currentJob: this.currentJob} + }) + return learnMoreModal.present() + }, getDateTime(time: any) { return DateTime.fromMillis(time).toISO() }, @@ -604,6 +616,10 @@ export default defineComponent({ </script> <style scoped> +.learn-more-text { + font-size: 14px; + cursor: pointer; +} section { margin-top: var(--spacer-sm); margin-bottom: var(--spacer-sm); diff --git a/src/components/LearnMoreModal.vue b/src/components/LearnMoreModal.vue new file mode 100644 index 00000000..877389e5 --- /dev/null +++ b/src/components/LearnMoreModal.vue @@ -0,0 +1,171 @@ +<template> + <ion-header> + <ion-toolbar> + <ion-buttons slot="start"> + <ion-button @click="closeModal()"> + <ion-icon slot="icon-only" :icon="closeOutline" /> + </ion-button> + </ion-buttons> + <ion-title>{{ translate("Learn more") }}</ion-title> + <ion-buttons slot="end"> + <ion-button fill="clear" color="medium" @click="redirectToJobsDoc()"> + <ion-icon slot="icon-only" :icon="openOutline" /> + </ion-button> + </ion-buttons> + </ion-toolbar> + </ion-header> + + <ion-content> + <div class="empty-state" v-if="isGeneratingAnswer"> + <ion-item lines="none"> + <ion-spinner name="crescent" slot="start" /> + {{ translate("Generating answer...") }} + </ion-item> + </div> + + <div class="empty-state" v-else-if="!Object.keys(askResponse).length"> + <ion-item lines="none"> + <p>{{ translate("The job details is not generating, please try again later.") }}</p> + </ion-item> + </div> + + <div v-else> + <ion-item lines="full" class="ion-margin-top"> + <ion-label> + {{ queryString }} + <p>{{ currentJob?.systemJobEnumId }}</p> + </ion-label> + </ion-item> + + <ion-list> + <ion-item lines="none"> + <ion-label>{{ translate("Sources") }}</ion-label> + </ion-item> + <ion-row class="ion-padding" v-for="section in jobSection" :key="section.id"> + <ion-chip outline @click="redirectToDoc(section)"> + <ion-label>{{ section.title }}</ion-label> + <ion-icon :icon="openOutline" /> + </ion-chip> + </ion-row> + </ion-list> + + <ion-item> + <ion-label> + <p class="overline">{{ translate("Summary") }}</p> + {{ askResponse?.text }} + </ion-label> + </ion-item> + </div> + </ion-content> +</template> + +<script lang="ts"> +import { IonButton, IonButtons, IonChip, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonList, IonRow, IonSpinner, IonTitle, IonToolbar, modalController } from "@ionic/vue"; +import { closeOutline, openOutline } from 'ionicons/icons' +import { translate } from '@hotwax/dxp-components'; +import { defineComponent } from "vue"; +import { hasError } from '@/utils' +import { askQuery, searchQuery } from "@/adapter"; +import logger from "@/logger"; + +export default defineComponent({ + name: "LearnMoreModal", + components: { + IonButton, + IonButtons, + IonChip, + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonList, + IonRow, + IonSpinner, + IonTitle, + IonToolbar + }, + data() { + return { + queryString: '', + askResponse: {} as any, + jobSection: {} as any, + isGeneratingAnswer: true + } + }, + props: ["currentJob"], + mounted() { + this.askQuery(); + }, + methods: { + async searchQuery(pageIds: any) { + let items = [] as any; + this.jobSection = []; + try { + const resp = await searchQuery({ + queryString: this.currentJob?.enumName, + spaceId: process.env.VUE_APP_SPACE_ID, + baseURL: process.env.VUE_APP_GITBOOK_BASE_URL, + token: process.env.VUE_APP_GITBOOK_API_KEY + }); + if(!hasError(resp)) { + items = resp.data.items; + pageIds.forEach((pageId: any) => { + const filteredPage = items.find((item: any) => item.id === pageId); + if (filteredPage) { + this.jobSection.push(...filteredPage.sections); + } + }); + this.isGeneratingAnswer = false; + } else { + throw resp.data; + } + } catch(error: any) { + logger.error(error); + this.isGeneratingAnswer = false; + } + }, + async askQuery() { + this.queryString = `What does ${this.currentJob?.enumName} job do?`; + try { + const resp = await askQuery({ + queryString: this.queryString, + spaceId: process.env.VUE_APP_SPACE_ID, + baseURL: process.env.VUE_APP_GITBOOK_BASE_URL, + token: process.env.VUE_APP_GITBOOK_API_KEY + }); + if(!hasError(resp)) { + this.askResponse = resp.data.answer; + if(this.askResponse) { + const pageIds = this.askResponse?.sources.map((source: any) => source.page); + this.searchQuery(pageIds); + } else { + this.isGeneratingAnswer = false; + } + } else { + throw resp.data; + } + } catch(error: any) { + logger.error(error); + this.isGeneratingAnswer = false; + } + }, + async redirectToDoc(section: any) { + window.open(`https://docs.hotwax.co/documents/retail-operations/${section.path}`, "_blank", "noopener, noreferrer") + }, + async redirectToJobsDoc() { + window.open(`${process.env.VUE_APP_GITBOOK_JOBS_DOCS_URL}`, "_blank", "noopener, noreferrer") + }, + async closeModal() { + modalController.dismiss({ dismissed: true }); + } + }, + setup() { + return { + closeOutline, + openOutline, + translate + }; + } +}) +</script> diff --git a/src/locales/en.json b/src/locales/en.json index 4f852fa1..26b52d54 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -105,6 +105,7 @@ "Fulfilled": "Fulfilled", "Fulfillment": "Fulfillment", "Fulfillment status": "Fulfillment status", + "Generating answer...": "Generating answer...", "Get Paid Transactions": "Get Paid Transactions", "Go to OMS": "Go to OMS", "Go to Launchpad": "Go to Launchpad", @@ -136,6 +137,7 @@ "Landed inventory cost": "Landed inventory cost", "Last run": "Last run", "Last Shopify Order ID": "Last Shopify Order ID", + "Learn more": "Learn more", "Loading": "Loading", "Login": "Login", "Log Id": "Log Id", @@ -269,6 +271,7 @@ "Skip job": "Skip job", "Skip once": "Skip once", "Skipping will run this job at the next occurrence based on the temporal expression.": "Skipping will run this job at the next occurrence based on the temporal expression.", + "Sources": "Sources", "Some jobs have slow frequency type, hence, feasible frequency will be set automatically": "Some jobs have slow frequency type, hence, feasible frequency will be set automatically", "Something went wrong": "Something went wrong", "Something went wrong while getting complete user permissions.": "Something went wrong while getting complete user permissions.", @@ -282,6 +285,7 @@ "store name": "store name", "Stores": "Stores", "stores selected": "stores selected", + "Summary": "Summary", "Sync": "Sync", "Sync pre-selling related information to Shopify as tags and meta fields.": "Sync pre-selling related information to Shopify as tags and meta fields.", "Sync products": "Sync products", @@ -302,6 +306,7 @@ "This job may take several minutes to run. Wait till the job has moved to the pipeline history before checking results.": "This job may take several minutes to run.{ space } Wait till the job has moved to the pipeline history before checking results.", "This job schedule cannot be skipped": "This job schedule cannot be skipped", "This job does not have any custom parameters.": "This job does not have any custom parameters.", + "The job details is not generating, please try again later.": "The job details is not generating, please try again later.", "Time zone updated successfully": "Time zone updated successfully", "Timezone": "Timezone", "Tomorrow": "Tomorrow", diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 60051585..3fa8beb7 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -184,7 +184,7 @@ const actions: ActionTree<UserState, RootState> = { commit(types.USER_PWA_STATE_UPDATED, payload); }, - async getShopifyConfig({ commit }, productStoreId) { + async getShopifyConfig({ commit, state }, productStoreId) { if (!productStoreId) { commit(types.USER_SHOPIFY_CONFIGS_UPDATED, []); commit(types.USER_CURRENT_SHOPIFY_CONFIG_UPDATED, {}); @@ -192,7 +192,7 @@ const actions: ActionTree<UserState, RootState> = { } try { - const shopifyConfigs = await UserService.getShopifyConfig(productStoreId); + const shopifyConfigs = await UserService.getShopifyConfig(productStoreId, state?.token); // TODO store and get preferred config let currentShopifyConfig = {}; shopifyConfigs.length > 0 && (currentShopifyConfig = shopifyConfigs[0]) diff --git a/src/views/DataManagerLogDetails.vue b/src/views/DataManagerLogDetails.vue index 1cc43044..7810aeed 100644 --- a/src/views/DataManagerLogDetails.vue +++ b/src/views/DataManagerLogDetails.vue @@ -92,10 +92,12 @@ <p>{{ translate('Finished') }}</p> </ion-label> - <ion-badge v-if="log.statusId" :color="log.statusId === 'SERVICE_FAILED' ? 'danger' : 'success'">{{ translate(getStatusDesc(log.statusId)) }}</ion-badge> + <ion-badge v-if="log.statusId" :color="getLogStatusColor(log.statusId)">{{ translate(getStatusDesc(log.statusId)) }}</ion-badge> - <div class="ion-text-center" lines="none" v-if="log.errorRecordContentId" button @click="downloadErrorRecordFile(log)"> - <ion-icon slot="start" :icon="cloudDownloadOutline" /> + <div class="ion-text-center" lines="none" v-if="log.errorRecordContentId"> + <ion-button fill="clear" color="medium" @click="downloadErrorRecordFile(log)"> + <ion-icon slot="icon-only" :icon="cloudDownloadOutline" /> + </ion-button> <ion-label> <p>{{ translate('Failed records') }}</p> </ion-label> @@ -257,6 +259,17 @@ export default defineComponent ({ } catch (error) { logger.error(error); } + }, + getLogStatusColor(statusId) { + if (statusId === 'SERVICE_FINISHED') { + return 'success'; + } else if (statusId === 'SERVICE_RUNNING') { + return 'dark'; + } else if (statusId === 'SERVICE_FAILED') { + return 'danger'; + } else { + return 'medium'; + } } }, setup() { diff --git a/src/views/Pipeline.vue b/src/views/Pipeline.vue index b1fdfe5c..6e20fd9c 100644 --- a/src/views/Pipeline.vue +++ b/src/views/Pipeline.vue @@ -72,11 +72,17 @@ <ion-label class="ion-text-wrap">{{ job.tempExprId ? temporalExpr(job.tempExprId)?.description : "🙃" }}</ion-label> </ion-item> - <ion-item lines="full"> + <ion-item> <ion-icon slot="start" :icon="refreshOutline" /> <ion-label class="ion-text-wrap">{{ job.currentRetryCount }}</ion-label> </ion-item> + <ion-item lines="full"> + <ion-icon slot="start" :icon="codeWorkingOutline" /> + <ion-label class="ion-text-wrap">{{ job.systemJobEnumId }}</ion-label> + <ion-icon :icon="helpCircleOutline" @click.stop.prevent="openLearnMoreModal(job)"/> + </ion-item> + <div class="actions"> <div> <ion-button :disabled="!hasPermission(Actions.APP_JOB_UPDATE)" fill="clear" @click.stop="skipJob(job)">{{ translate("Skip") }}</ion-button> @@ -306,7 +312,7 @@ import { IonButtons } from "@ionic/vue"; import JobConfiguration from '@/components/JobConfiguration.vue' -import { closeCircleOutline, codeWorkingOutline, copyOutline, ellipsisVerticalOutline, filterOutline, pinOutline, refreshOutline, timeOutline, timerOutline } from "ionicons/icons"; +import { closeCircleOutline, codeWorkingOutline, copyOutline, ellipsisVerticalOutline, filterOutline, helpCircleOutline, pinOutline, refreshOutline, timeOutline, timerOutline } from "ionicons/icons"; import emitter from '@/event-bus'; import JobHistoryModal from '@/components/JobHistoryModal.vue'; import { Plugins } from '@capacitor/core'; @@ -316,6 +322,7 @@ import { Actions, hasPermission } from '@/authorization' import Filters from '@/components/Filters.vue'; import FailedJobReasonModal from '@/views/FailedJobReasonModal.vue' import { translate } from '@hotwax/dxp-components'; +import LearnMoreModal from '@/components/LearnMoreModal.vue'; export default defineComponent({ name: "Pipeline", @@ -398,6 +405,13 @@ export default defineComponent({ this.isScrollingEnabled = false; }, methods : { + async openLearnMoreModal(job: any) { + const learnMoreModal = await modalController.create({ + component: LearnMoreModal, + componentProps: {currentJob: job} + }) + return learnMoreModal.present() + }, isPinnedJobSelected(jobEnumId: any) { return (this as any).pipelineFilters.enum.some((jobId: any) => jobId === jobEnumId ); }, @@ -690,6 +704,7 @@ export default defineComponent({ store, codeWorkingOutline, ellipsisVerticalOutline, + helpCircleOutline, pinOutline, refreshOutline, timeOutline,