Browse Source

feat: update workflow engine

Ahmad Kholid 2 years ago
parent
commit
d757d383b7

+ 22 - 19
src/background/BackgroundWorkflowTriggers.js

@@ -1,29 +1,32 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this */
 import browser from 'webextension-polyfill';
 import BackgroundUtils from './BackgroundUtils';
 
 class BackgroundWorkflowTriggers {
-	async visitWebTriggers(tabId, tabUrl) {
-	  const { visitWebTriggers } = await browser.storage.local.get('visitWebTriggers');
-	  if (!visitWebTriggers || visitWebTriggers.length === 0) return;
+  async visitWebTriggers(tabId, tabUrl) {
+    const { visitWebTriggers } = await browser.storage.local.get(
+      'visitWebTriggers'
+    );
+    if (!visitWebTriggers || visitWebTriggers.length === 0) return;
 
-	  const triggeredWorkflow = visitWebTriggers.find(({ url, isRegex }) => {
-	    if (url.trim() === '') return false;
+    const triggeredWorkflow = visitWebTriggers.find(({ url, isRegex }) => {
+      if (url.trim() === '') return false;
 
-	    return tabUrl.match(isRegex ? new RegExp(url, 'g') : url);
-	  });
+      return tabUrl.match(isRegex ? new RegExp(url, 'g') : url);
+    });
 
-	  if (triggeredWorkflow) {
-	    let workflowId = triggeredWorkflow.id;
-	    if (triggeredWorkflow.id.startsWith('trigger')) {
-	      const { 1: triggerWorkflowId } = triggeredWorkflow.id.split(':');
-	      workflowId = triggerWorkflowId;
-	    }
+    if (triggeredWorkflow) {
+      let workflowId = triggeredWorkflow.id;
+      if (triggeredWorkflow.id.startsWith('trigger')) {
+        const { 1: triggerWorkflowId } = triggeredWorkflow.id.split(':');
+        workflowId = triggerWorkflowId;
+      }
 
-	    const workflowData = await BackgroundUtils.getWorkflow(workflowId);
-	    if (workflowData) BackgroundUtils.executeWorkflow(workflowData, { tabId });
-	  }
-	}
+      const workflowData = await BackgroundUtils.getWorkflow(workflowId);
+      if (workflowData)
+        BackgroundUtils.executeWorkflow(workflowData, { tabId });
+    }
+  }
 }
 
-export default BackgroundWorkflowTriggers;
+export default BackgroundWorkflowTriggers;

+ 0 - 1
src/background/workflowEngine/blocksHandler/handlerExecuteWorkflow.js

@@ -1,4 +1,3 @@
-/* eslint-disable */
 import browser from 'webextension-polyfill';
 import { isWhitespace, parseJSON } from '@/utils/helper';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';

+ 3 - 1
src/components/newtab/logs/LogsHistory.vue

@@ -47,7 +47,9 @@
           <ui-popover trigger-width class="mr-4">
             <template #trigger>
               <ui-button>
-                <span> Export <span class="hidden lg:block">logs</span> </span>
+                <span>
+                  Export <span class="hidden lg:inline-block">logs</span>
+                </span>
                 <v-remixicon name="riArrowDropDownLine" class="ml-2 -mr-1" />
               </ui-button>
             </template>

+ 8 - 12
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -109,7 +109,7 @@
         })`
       "
       class="hoverable p-2 rounded-lg"
-      @click="executeWorkflow"
+      @click="executeCurrWorkflow"
     >
       <v-remixicon name="riPlayLine" />
     </button>
@@ -285,7 +285,6 @@ import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 import { useToast } from 'vue-toastification';
 import browser from 'webextension-polyfill';
-import { sendMessage } from '@/utils/message';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
@@ -299,6 +298,7 @@ import { tagColors } from '@/utils/shared';
 import { parseJSON, findTriggerBlock } from '@/utils/helper';
 import { exportWorkflow, convertWorkflow } from '@/utils/workflowData';
 import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import getTriggerText from '@/utils/triggerText';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';
@@ -344,7 +344,7 @@ const shortcuts = useShortcut([
   /* eslint-disable-next-line */
   getShortcut('editor:save', saveWorkflow),
   /* eslint-disable-next-line */
-  getShortcut('editor:execute-workflow', executeWorkflow),
+  getShortcut('editor:execute-workflow', executeCurrWorkflow),
 ]);
 
 const { teamId } = router.currentRoute.value.params;
@@ -405,15 +405,11 @@ function updateWorkflowDescription(value) {
   updateWorkflow(payload);
   state.showEditDescription = false;
 }
-function executeWorkflow() {
-  sendMessage(
-    'workflow:execute',
-    {
-      ...props.workflow,
-      isTesting: props.isDataChanged,
-    },
-    'background'
-  );
+function executeCurrWorkflow() {
+  executeWorkflow({
+    ...props.workflow,
+    isTesting: props.isDataChanged,
+  });
 }
 async function setAsHostWorkflow(isHost) {
   if (!userStore.user) {

+ 1 - 4
src/components/newtab/workflows/WorkflowsHosted.vue

@@ -13,9 +13,9 @@
 import { computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
-import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const props = defineProps({
@@ -66,7 +66,4 @@ async function deleteWorkflow(workflow) {
     },
   });
 }
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
 </script>

+ 1 - 4
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -107,12 +107,12 @@ import SelectionArea from '@viselect/vanilla';
 import browser from 'webextension-polyfill';
 import cloneDeep from 'lodash.clonedeep';
 import { arraySorter } from '@/utils/helper';
-import { sendMessage } from '@/utils/message';
 import { useUserStore } from '@/stores/user';
 import { useDialog } from '@/composable/dialog';
 import { useWorkflowStore } from '@/stores/workflow';
 import { exportWorkflow } from '@/utils/workflowData';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import WorkflowsLocalCard from './WorkflowsLocalCard.vue';
 
 const props = defineProps({
@@ -230,9 +230,6 @@ const pinnedWorkflows = computed(() => {
   });
 });
 
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
 function toggleDisableWorkflow({ id, isDisabled }) {
   workflowStore.update({
     id,

+ 1 - 5
src/components/newtab/workflows/WorkflowsShared.vue

@@ -11,8 +11,8 @@
 <script setup>
 import { computed } from 'vue';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
-import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const props = defineProps({
@@ -42,8 +42,4 @@ const workflows = computed(() => {
     order: props.sort.order,
   });
 });
-
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
 </script>

+ 1 - 4
src/components/newtab/workflows/WorkflowsUserTeam.vue

@@ -58,10 +58,10 @@ import { useToast } from 'vue-toastification';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
 import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
-import { sendMessage } from '@/utils/message';
 import { arraySorter } from '@/utils/helper';
 import { useDialog } from '@/composable/dialog';
 import { tagColors } from '@/utils/shared';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const props = defineProps({
@@ -149,9 +149,6 @@ const workflows = computed(() => {
   });
 });
 
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
 function onMenuSelected({ id, data }) {
   if (id === 'delete') {
     dialog.confirm({

+ 1 - 3
src/components/popup/home/HomeTeamWorkflows.vue

@@ -35,6 +35,7 @@ import { useUserStore } from '@/stores/user';
 import { sendMessage } from '@/utils/message';
 import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { tagColors } from '@/utils/shared';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import dayjs from '@/lib/dayjs';
 
 const props = defineProps({
@@ -59,9 +60,6 @@ function openWorkflowPage({ teamId, id }) {
   const url = `/teams/${teamId}/workflows/${id}`;
   sendMessage('open:dashboard', url, 'background');
 }
-function executeWorkflow(workflow) {
-  sendMessage('workflow:execute', workflow, 'background');
-}
 
 onMounted(() => {
   if (!userStore.user?.teams) return;

+ 0 - 9
src/newtab/App.vue

@@ -64,7 +64,6 @@ import { usePackageStore } from '@/stores/package';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
 import { useTheme } from '@/composable/theme';
-import { parseJSON } from '@/utils/helper';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
@@ -181,14 +180,6 @@ async function syncHostedWorkflows() {
   await hostedWorkflowStore.fetchWorkflows(hostIds);
 }
 
-window.addEventListener('storage', ({ key, newValue }) => {
-  if (key !== 'workflowState') return;
-
-  const states = parseJSON(newValue, {});
-  workflowStore.states = Object.values(states).filter(
-    ({ isDestroyed }) => !isDestroyed
-  );
-});
 browser.runtime.onMessage.addListener(({ type, data }) => {
   if (type === 'refresh-packages') {
     packageStore.loadData(true);

+ 5 - 5
src/newtab/pages/workflows/Host.vue

@@ -61,7 +61,7 @@
             })`
           "
           class="p-2 hoverable rounded-lg"
-          @click="executeWorkflow"
+          @click="executeCurrWorkflow"
         >
           <v-remixicon name="riPlayLine" />
         </button>
@@ -114,13 +114,13 @@
 import { computed, reactive, onMounted, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute, useRouter } from 'vue-router';
-import { sendMessage } from '@/utils/message';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 import { findTriggerBlock } from '@/utils/helper';
 import convertWorkflowData from '@/utils/convertWorkflowData';
 import { useWorkflowStore } from '@/stores/workflow';
+import { executeWorkflow } from '@/newtab/utils/workflowEngine';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
 import getTriggerText from '@/utils/triggerText';
 import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
@@ -146,7 +146,7 @@ const editorOptions = {
 };
 
 /* eslint-disable-next-line */
-const shortcut = useShortcut('editor:execute-workflow', executeWorkflow);
+const shortcut = useShortcut('editor:execute-workflow', executeCurrWorkflow);
 
 const state = reactive({
   editorKey: 0,
@@ -199,13 +199,13 @@ async function deleteWorkflowHost() {
     },
   });
 }
-function executeWorkflow() {
+function executeCurrWorkflow() {
   const payload = {
     ...workflow.value,
     id: workflowId,
   };
 
-  sendMessage('workflow:execute', payload, 'background');
+  executeWorkflow(payload);
 }
 async function retrieveTriggerText() {
   const triggerBlock = findTriggerBlock(workflow.value.drawflow);

+ 20 - 0
src/newtab/utils/workflowEngine/WorkflowLogger.js

@@ -0,0 +1,20 @@
+import dbLogs, { defaultLogItem } from '@/db/logs';
+/* eslint-disable class-methods-use-this */
+class WorkflowLogger {
+  constructor({ key = 'logs' }) {
+    this.key = key;
+  }
+
+  async add({ detail, history, ctxData, data }) {
+    const logDetail = { ...defaultLogItem, ...detail };
+
+    await Promise.all([
+      dbLogs.logsData.add(data),
+      dbLogs.ctxData.add(ctxData),
+      dbLogs.items.add(logDetail),
+      dbLogs.histories.add(history),
+    ]);
+  }
+}
+
+export default WorkflowLogger;

+ 91 - 0
src/newtab/utils/workflowEngine/WorkflowState.js

@@ -0,0 +1,91 @@
+/* eslint-disable  no-param-reassign */
+
+class WorkflowState {
+  constructor({ storage, key = 'workflowState' }) {
+    this.key = key;
+    this.storage = storage;
+
+    this.states = new Map();
+    this.eventListeners = {};
+  }
+
+  _saveToStorage() {
+    const states = Object.fromEntries(this.states);
+    return this.storage.set(this.key, states);
+  }
+
+  dispatchEvent(name, params) {
+    const listeners = this.eventListeners[name];
+
+    if (!listeners) return;
+
+    listeners.forEach((callback) => {
+      callback(params);
+    });
+  }
+
+  on(name, listener) {
+    (this.eventListeners[name] = this.eventListeners[name] || []).push(
+      listener
+    );
+  }
+
+  off(name, listener) {
+    const listeners = this.eventListeners[name];
+    if (!listeners) return;
+
+    const index = listeners.indexOf(listener);
+    if (index !== -1) listeners.splice(index, 1);
+  }
+
+  get getAll() {
+    return this.states;
+  }
+
+  async get(stateId) {
+    let { states } = this;
+
+    if (typeof stateId === 'function') {
+      states = Array.from(states.entries()).find(({ 1: state }) =>
+        stateId(state)
+      );
+    } else if (stateId) {
+      states = this.states.get(stateId);
+    }
+
+    return states;
+  }
+
+  async add(id, data = {}) {
+    this.states.set(id, data);
+    await this._saveToStorage(this.key);
+  }
+
+  async stop(id) {
+    const isStateExist = await this.get(id);
+    if (!isStateExist) {
+      await this.delete(id);
+      this.dispatchEvent('stop', id);
+      return id;
+    }
+
+    await this.update(id, { isDestroyed: true });
+    this.dispatchEvent('stop', id);
+    return id;
+  }
+
+  async update(id, data = {}) {
+    const state = this.states.get(id);
+    this.states.set(id, { ...state, ...data });
+    this.dispatchEvent('update', { id, data });
+    await this._saveToStorage();
+  }
+
+  async delete(id) {
+    this.states.delete(id);
+    this.dispatchEvent('delete', id);
+    await this._saveToStorage();
+  }
+}
+
+export default WorkflowState;

+ 1 - 2
src/newtab/utils/workflowEngine/blocksHandler/handlerExecuteWorkflow.js

@@ -1,9 +1,8 @@
-/* eslint-disable */
 import browser from 'webextension-polyfill';
 import { isWhitespace, parseJSON } from '@/utils/helper';
 import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
 import convertWorkflowData from '@/utils/convertWorkflowData';
-import WorkflowEngine from '../engine';
+import WorkflowEngine from '../WorkflowEngine';
 
 function workflowListener(workflow, options) {
   return new Promise((resolve, reject) => {

+ 152 - 0
src/newtab/utils/workflowEngine/index.js

@@ -0,0 +1,152 @@
+import browser from 'webextension-polyfill';
+import dayjs from '@/lib/dayjs';
+import { useWorkflowStore } from '@/stores/workflow';
+import decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';
+import { parseJSON } from '@/utils/helper';
+import { fetchApi } from '@/utils/api';
+import getBlockMessage from '@/utils/getBlockMessage';
+import convertWorkflowData from '@/utils/convertWorkflowData';
+import WorkflowState from './WorkflowState';
+import WorkflowLogger from './WorkflowLogger';
+import WorkflowEngine from './WorkflowEngine';
+import blocksHandler from './blocksHandler';
+
+const workflowStateStorage = {
+  get() {
+    const workflowStore = useWorkflowStore();
+    return workflowStore.states;
+  },
+  set(key, value) {
+    const workflowStore = useWorkflowStore();
+    workflowStore.updateStates(Object.values(value));
+  },
+};
+const browserStorage = {
+  async get(key) {
+    try {
+      const result = await browser.storage.local.get(key);
+
+      return result[key];
+    } catch (error) {
+      console.error(error);
+      return [];
+    }
+  },
+  async set(key, value) {
+    await browser.storage.local.set({ [key]: value });
+
+    if (key === 'workflowState') {
+      sessionStorage.setItem(key, JSON.stringify(value));
+    }
+  },
+};
+
+export const workflowLogger = new WorkflowLogger({ storage: browserStorage });
+export const workflowState = new WorkflowState({
+  storage: workflowStateStorage,
+});
+
+export function executeWorkflow(workflowData, options) {
+  if (workflowData.isDisabled) return null;
+  if (workflowData.isProtected) {
+    const flow = parseJSON(workflowData.drawflow, null);
+
+    if (!flow) {
+      const pass = getWorkflowPass(workflowData.pass);
+
+      workflowData.drawflow = decryptFlow(workflowData, pass);
+    }
+  }
+
+  const convertedWorkflow = convertWorkflowData(workflowData);
+  const engine = new WorkflowEngine(convertedWorkflow, {
+    options,
+    states: workflowState,
+    logger: workflowLogger,
+    blocksHandler: blocksHandler(),
+  });
+
+  engine.init();
+  engine.on(
+    'destroyed',
+    ({
+      id,
+      status,
+      history,
+      startedTimestamp,
+      endedTimestamp,
+      blockDetail,
+    }) => {
+      if (workflowData.id.startsWith('team') && workflowData.teamId) {
+        const payload = {
+          status,
+          workflowId: workflowData.id,
+          workflowLog: {
+            status,
+            endedTimestamp,
+            startedTimestamp,
+          },
+        };
+
+        if (status === 'error') {
+          const message = getBlockMessage(blockDetail);
+          const workflowHistory = history.map((item) => {
+            delete item.logId;
+            delete item.prevBlockData;
+            delete item.workerId;
+
+            item.description = item.description || '';
+
+            return item;
+          });
+          payload.workflowLog = {
+            status,
+            message,
+            endedTimestamp,
+            startedTimestamp,
+            history: workflowHistory,
+            blockId: blockDetail.blockId,
+          };
+        }
+
+        fetchApi(`/teams/${workflowData.teamId}/workflows/logs`, {
+          method: 'POST',
+          body: JSON.stringify(payload),
+        }).catch((error) => {
+          console.error(error);
+        });
+      }
+
+      if (status !== 'stopped') {
+        browser.permissions
+          .contains({ permissions: ['notifications'] })
+          .then((hasPermission) => {
+            if (!hasPermission || !workflowData.settings.notification) return;
+
+            const name = workflowData.name.slice(0, 32);
+
+            browser.notifications.create(`logs:${id}`, {
+              type: 'basic',
+              iconUrl: browser.runtime.getURL('icon-128.png'),
+              title: status === 'success' ? 'Success' : 'Error',
+              message: `${
+                status === 'success' ? 'Successfully' : 'Failed'
+              } to run the "${name}" workflow`,
+            });
+          });
+      }
+    }
+  );
+
+  const lastCheckStatus = localStorage.getItem('check-status');
+  const isSameDay = dayjs().isSame(lastCheckStatus, 'day');
+  if (!isSameDay) {
+    fetchApi('/status')
+      .then((response) => response.json())
+      .then(() => {
+        localStorage.setItem('check-status', new Date());
+      });
+  }
+
+  return engine;
+}

+ 3 - 0
src/stores/workflow.js

@@ -131,6 +131,9 @@ export const useWorkflowStore = defineStore('workflow', {
 
       this.retrieved = true;
     },
+    updateStates(newStates) {
+      this.states = newStates;
+    },
     async insert(data = {}, options = {}) {
       const insertedWorkflows = {};