Source: management-system/src/frontend/views/ProjectOverview.vue

<template>
  <div class="editor-explorer">
    <AlertWindow :popupData="popupData" />
    <InactivityAlertWindow :popupData="inactivityWarningData" />
    <deployment-toolbar
      :title="project.name"
      :shortTitle="`Project code: ${processMetaData.orderCode} | Customer: ${processMetaData.customerName} | Order number: ${processMetaData.orderNumber}`"
    >
      <v-badge inline left color="grey">{{ project.planningStatus || 'Planning Status' }}</v-badge>
      <v-badge class="ml-2" inline left color="grey">{{
        project.scheduleStatus || 'Schedule Status'
      }}</v-badge>
    </deployment-toolbar>

    <InstanceActionToolbar
      :colorsShown.sync="colorsShown"
      :instanceId="instance ? instance.processInstanceId : null"
    >
      <div class="d-inline-flex action-toolbar-slot">
        <v-btn color="primary" @click="editBpmn">Edit</v-btn>
        <v-divider vertical></v-divider>
        <v-tooltip :disabled="engineOnline" right>
          <template v-slot:activator="{ on }">
            <div v-on="on">
              <div v-if="projectStarted">
                <v-btn
                  color="orange"
                  class="white--text"
                  v-if="!isProjectPaused && !isProjectPausing && !isProjectStopped"
                  :loading="showPauseDialog"
                  :disabled="!isProjectRunning || !engineOnline"
                  @click="showPauseDialog = true"
                  >Pause</v-btn
                >
                <v-btn
                  color="success"
                  v-if="isProjectPaused || isProjectPausing"
                  :loading="showResumeDialog"
                  :disabled="!engineOnline"
                  @click="showResumeDialog = true"
                  >Resume</v-btn
                >
                <v-btn
                  color="error"
                  v-if="!isProjectStopped"
                  :loading="showStopDialog"
                  :disabled="!isProjectRunning || !engineOnline"
                  @click="showStopDialog = true"
                  >Cancel</v-btn
                >
                <v-btn
                  color="primary"
                  v-if="isProjectStopped"
                  :loading="showRestartDialog"
                  :disabled="!engineOnline"
                  @click="showRestartDialog = true"
                  >Restart</v-btn
                >
              </div>
              <v-btn
                v-else
                color="primary"
                :disabled="!engineOnline"
                :loading="startingProject"
                @click="startProject"
                >Start</v-btn
              >
            </div>
          </template>
          <span>Project Engine is not reachable</span>
        </v-tooltip>
        <v-divider vertical></v-divider>
        <div v-if="isProjectPausing" class="py-1">
          <v-progress-circular indeterminate color="black" size="28" width="2">
            <v-tooltip right>
              <template v-slot:activator="{ on }">
                <v-icon v-on="on" @click="resumeProject()">mdi-close</v-icon>
              </template>
              Abort pausing
            </v-tooltip>
          </v-progress-circular>
          Pausing...
        </div>
      </div>
    </InstanceActionToolbar>
    <execution-overview
      v-if="deployment"
      :deployment="deployment"
      :instance="instance"
      :colorsShown="colorsShown"
      :location="location"
      :isProjectView="true"
      @element:click="handleElementClick"
      @deleteDeployment="showDeleteDeploymentDialog = true"
    ></execution-overview>

    <confirmation
      title="cancel the project?"
      continueButtonText="Cancel Project"
      continueButtonColor="error"
      :show="showStopDialog"
      maxWidth="500px"
      @cancel="showStopDialog = false"
      @continue="stopProject"
    >
      <div>
        Currently running Scripts can not be stopped, thus they are executed until their end.
      </div>
    </confirmation>

    <confirmation
      title="restart the project?"
      continueButtonText="Restart Project"
      continueButtonColor="primary"
      :show="showRestartDialog"
      maxWidth="500px"
      @cancel="showRestartDialog = false"
      @continue="restartProject"
    >
      <div>
        Project will restart from beginning. Information about current project will be deleted
        irrecoverably.
      </div>
    </confirmation>

    <confirmation
      title="pause the project?"
      continueButtonText="Pause Project"
      continueButtonColor="primary"
      :show="showPauseDialog"
      maxWidth="500px"
      @cancel="showPauseDialog = false"
      @continue="pauseProject"
    >
      <div>
        Hint: Currently running activities can not be paused. Therefore the Process is paused after
        the running activities finished their task.
      </div>
    </confirmation>

    <confirmation
      title="resume the project?"
      continueButtonText="Resume Project"
      continueButtonColor="primary"
      :show="showResumeDialog"
      maxWidth="500px"
      @cancel="showResumeDialog = false"
      @continue="resumeProject"
    >
      <div>Paused tokens will resume at their current location.</div>
    </confirmation>

    <confirmation
      title="Delete the deployment?"
      continueButtonText="Delete Deployment"
      continueButtonColor="primary"
      :show="showDeleteDeploymentDialog"
      maxWidth="500px"
      @cancel="showDeleteDeploymentDialog = false"
      @continue="deleteDeployment"
    >
      <div>
        You are about to delete the Deployment! Information of current project will be deleted
        irrecoverably.
      </div>
    </confirmation>
  </div>
</template>

<script>
import {
  getAllBpmnFlowNodeIds,
  setMachineInfo,
  getMetaData,
  getProcessIds,
} from '@proceed/bpmn-helper';
import { engineNetworkInterface } from '../backend-api/index.js';
import AlertWindow from '@/frontend/components/universal/Alert.vue';
import InactivityAlertWindow from '@/frontend/components/universal/InactivityAlert.vue';
import * as customCommands from '../helpers/bpmn-modeler-events/custom-modeler-commands.js';
import Confirmation from '@/frontend/components/universal/Confirmation.vue';
import DeploymentToolbar from '@/frontend/components/deployments/DeploymentToolbar.vue';
import InstanceActionToolbar from '@/frontend/components/deployments/InstanceActionToolbar.vue';
import ExecutionOverview from '@/frontend/components/deployments/ExecutionOverview.vue';
/**
 * @module views
 */
/**
 * @memberof module:views
 */
/**
 * This view is opened when you click on the "Open Project" in the Projects views.
 * It shows the bpmn diagram of a project (without the option to edit it)
 * If the project was started (it was deployed and an instance exists), instance info
 * is also shown.
 *
 * @module Vue:ProjectOverview
 *
 * @vue-computed {Object} project
 * @vue-computed {Object} selectedElement - the activity/event for which detailed info should be shown
 * @vue-computed {String} startTime - the time when the project was started, empty if hasn't started yet
 * @vue-computed {Boolean} isProjectRunning - if yes, the stop instance button is enabled
 * @vue-computed {String} engineUrl - the address of the engine on which the Project is supposed to be executed
 * @vue-computed {String} engineHost - the ip-address of the engine
 * @vue-computed {String} enginePort - the port of the engine
 */

export default {
  name: 'project-overview',
  components: {
    AlertWindow,
    InactivityAlertWindow,
    Confirmation,
    ExecutionOverview,
    DeploymentToolbar,
    InstanceActionToolbar,
  },
  data() {
    return {
      clickedElement: null,
      showStopDialog: false,
      showRestartDialog: false,
      showPauseDialog: false,
      showResumeDialog: false,
      showDeleteDeploymentDialog: false,
      startingProject: false,
      popupData: {
        body: '',
        display: 'none',
        color: '',
        metaData: '',
      },
      inactivityWarningData: {
        display: 'none',
        color: 'info',
      },
      instance: null,
      deployment: null,
      subprocessId: null,
      pollingInterval: null,
      location: {
        timeZone: 'Europe/Berlin',
      },
      colorsShown: 'processColors',
      isInfoPanelVisible: false,
      engineOnline: false,
      metaData: {},
      processMetaData: {},
    };
  },
  computed: {
    project() {
      return this.$store.getters['processStore/processById'](this.$router.currentRoute.params.id);
    },
    selectedElement() {
      return this.clickedElement ? this.clickedElement : null;
    },
    startTime() {
      if (this.instance) {
        return new Date(this.instance.globalStartTime).toLocaleString('en-US', this.location);
      }
      return '';
    },
    projectStarted() {
      return !!(this.instance && Object.keys(this.instance).length > 0);
    },
    isProjectRunning() {
      const runningStates = ['RUNNING', 'READY', 'DEPLOYMENT-WAITING'];
      if (this.instance) {
        return this.instance.instanceState.some((state) => runningStates.includes(state));
      } else {
        return false;
      }
    },
    isProjectPausing() {
      if (this.instance) {
        return this.instance.instanceState.some((state) => state === 'PAUSING');
      } else {
        return false;
      }
    },
    isProjectPaused() {
      if (this.instance) {
        return this.instance.instanceState.some((state) => state === 'PAUSED');
      } else {
        return false;
      }
    },
    isProjectStopped() {
      if (this.instance) {
        return this.instance.instanceState.some((state) => state === 'STOPPED');
      } else {
        return false;
      }
    },
    engineUrl() {
      return this.$store.getters['configStore/config'].processEngineUrl;
    },
    engineHost() {
      return this.engineUrl.split(':')[0];
    },
    enginePort() {
      return this.engineUrl.split(':')[1];
    },
  },
  watch: {
    project: {
      async handler(newProject) {
        const newXml = await this.$store.getters['processStore/xmlById'](
          this.$router.currentRoute.params.id
        );

        const [processId] = await getProcessIds(newXml);

        this.processMetaData = await getMetaData(newXml, processId);

        this.deployment = {
          ...this.deployment,
          definitionName: newProject.name,
          bpmn: newXml,
        };
      },
      immediate: true,
    },
  },
  methods: {
    /**
     * Opens the ProcesBpmnEditor view
     */
    editBpmn() {
      this.$router.push({ name: 'edit-project-bpmn', params: { id: this.project.id } });
    },
    /**
     * Deploys the project statically using the engineUrl as machine address and starts an instance
     */
    async startProject() {
      if (this.engineUrl) {
        this.startingProject = true;
        const elements = await getAllBpmnFlowNodeIds(this.deployment.bpmn);
        const machineMapping = {};
        elements.forEach((el) => {
          machineMapping[el] = {
            machineAddress: this.engineUrl,
          };
        });
        const deployProcessXml = await setMachineInfo(this.deployment.bpmn, machineMapping);
        await this.$store.dispatch('processStore/updateWholeXml', {
          id: this.project.id,
          bpmn: deployProcessXml,
        });

        if (!this.project.shared) {
          this.popupData.body = 'Sending Project to Server to enable execution...';
          this.popupData.color = 'primary';
          this.popupData.display = 'block';

          await this.$store.dispatch('processStore/update', {
            id: this.project.id,
            changes: { shared: true },
          });
        }

        setTimeout(async () => {
          try {
            await engineNetworkInterface.deployProcess(this.project.id, false);
            try {
              await engineNetworkInterface.startInstance(this.project.id);
              this.popupData.body = 'Project started successfully';
              this.popupData.color = 'success';
              this.getInstanceInfo();
              this.startPollingInstanceInfo();
            } catch (err2) {
              this.popupData.body = `Failed to start process instance: ${err2.message}`;
              this.popupData.color = 'error';
            }
          } catch (err) {
            this.popupData.body = `Failed to deploy process: ${err.message}`;
            this.popupData.color = 'error';
          }
          this.startingProject = false;
          this.popupData.display = 'block';
          setTimeout(() => {
            this.popupData.display = 'none';
          }, 5000);
        }, 3000);
      }
    },
    /**
     * Stops the project execution
     */
    async stopProject() {
      this.showStopDialog = false;
      try {
        await engineNetworkInterface.stopInstance(this.project.id, this.instance.processInstanceId);
        this.popupData.body = 'Canceled project';
        this.popupData.color = 'primary';
      } catch (err) {
        this.popupData.body = `Error stopping the project: ${err.message}`;
        this.popupData.color = 'error';
        console.error(err);
      }
      this.popupData.display = 'block';
      setTimeout(() => {
        this.popupData.display = 'none';
      }, 5000);
    },
    /**
     * Restarts the stopped project execution
     */
    async restartProject() {
      this.showRestartDialog = false;
      try {
        await engineNetworkInterface.startInstance(this.project.id);
        this.popupData.body = 'Project restarted successfully';
        this.popupData.color = 'success';
      } catch (err) {
        this.popupData.body = `Error restarting the project: ${err.message}`;
        this.popupData.color = 'error';
      }
      this.popupData.display = 'block';
      setTimeout(() => {
        this.popupData.display = 'none';
      }, 5000);
    },
    /**
     * Pauses the project execution
     */
    async pauseProject() {
      this.showPauseDialog = false;
      try {
        await engineNetworkInterface.pauseInstance(
          this.project.id,
          this.instance.processInstanceId
        );
        this.popupData.body = 'Paused project';
        this.popupData.color = 'primary';
      } catch (err) {
        this.popupData.body = `Error pausing the project: ${err.message}`;
        this.popupData.color = 'error';
        console.error(err);
      }
      this.popupData.display = 'block';
      setTimeout(() => {
        this.popupData.display = 'none';
      }, 5000);
    },
    /**
     * Pauses the project execution
     */
    async resumeProject() {
      this.showResumeDialog = false;
      try {
        await engineNetworkInterface.resumeInstance(
          this.project.id,
          this.instance.processInstanceId
        );
        this.popupData.body = 'Resumed project';
        this.popupData.color = 'primary';
      } catch (err) {
        this.popupData.body = `Error resuming the project: ${err.message}`;
        this.popupData.color = 'error';
        console.error(err);
      }
      this.popupData.display = 'block';
      setTimeout(() => {
        this.popupData.display = 'none';
      }, 5000);
    },
    /**
     * If the clicked element is not the whole process or a sequence flow, set the clicked element and get its meta information
     * @param {Object} element - the clicked element
     */
    handleElementClick(element) {
      if (element.type === 'bpmn:Process' || element.type === 'bpmn:SequenceFlow') {
        this.clickedElement = null;
      } else {
        this.clickedElement = element;
      }
    },
    async deleteDeployment() {
      this.$store.dispatch('instanceStore/removeInstance', this.project.id);
      const response = await engineNetworkInterface.removeDeployment(this.project.id);
      this.instance = null;
      clearInterval(this.pollingInterval);
      this.showDeleteDeploymentDialog = false;
    },
    /**
     * If an instance exists, fetch info about it from the given engine
     */
    async getInstanceInfo() {
      const machine = {
        ip: this.engineHost,
        port: this.enginePort,
      };
      try {
        const instances = await engineNetworkInterface.getProcessInstances(
          machine,
          this.project.id
        );

        const instanceId = instances.pop();
        if (instanceId) {
          this.instance = await engineNetworkInterface.getInstanceInformation(
            machine,
            this.project.id,
            instanceId
          );
          this.instance.machines = [
            ...new Map(this.instance.log.map((l) => [l.machine.id, l.machine])).values(),
          ];
          this.$store.dispatch('instanceStore/addInstanceInformation', {
            definitionId: this.project.id,
            ...this.instance,
          });
        } else {
          this.instance = null;
          clearInterval(this.pollingInterval);
        }
        this.engineOnline = true;
      } catch (error) {
        this.engineOnline = false;
      }
    },
    /**
     * Call the function to get updated info about an existing instance every 5 seconds
     */
    startPollingInstanceInfo() {
      this.pollingInterval = setInterval(async () => {
        this.getInstanceInfo();
      }, 5000);
    },
  },
  async beforeMount() {
    const routerProcessDefinitionsId = this.$router.currentRoute.params.id;
    const bpmn = await this.$store.getters['processStore/xmlById'](routerProcessDefinitionsId);
    this.deployment = {
      bpmn,
      definitionId: routerProcessDefinitionsId,
    };

    const instance = await this.$store.getters['instanceStore/instanceById'](
      routerProcessDefinitionsId
    );

    if (instance) {
      this.instance = instance;
    }

    try {
      const deployments = await engineNetworkInterface.getDeployedProcesses({
        ip: this.engineHost,
        port: this.enginePort,
      });
      if (deployments.find((d) => d.definitionId == routerProcessDefinitionsId)) {
        this.deployment = deployments.find((d) => d.definitionId == routerProcessDefinitionsId);
        this.getInstanceInfo();
        this.startPollingInstanceInfo();
      }
      this.engineOnline = true;
    } catch (err) {
      if (this.isProjectRunning) {
        this.popupData.body = `Could not fetch current project status from Engine ${this.engineUrl}. Showing latest known state of project`;
        this.popupData.color = 'error';
        this.popupData.display = 'block';
      }
      setTimeout(() => {
        this.popupData.display = 'none';
      }, 5000);
    }
  },
  created() {
    if (this.$store.getters['warningStore/showWarning'] == true) {
      this.inactivityWarningData.display = 'block';
      this.$store.commit('warningStore/setWarning', false);
    }
  },
  mounted() {
    engineNetworkInterface.subscribeForDeploymentsUpdates();
    engineNetworkInterface.startMachinePolling();
  },
  beforeRouteLeave(to, from, next) {
    clearInterval(this.pollingInterval);
    next();
  },
  beforeDestroy() {
    clearInterval(this.pollingInterval);
  },
};
</script>

<style lang="scss" scoped>
.action-toolbar-slot .v-btn {
  width: 100px;
  margin: 0px 5px;
}

.action-toolbar-slot .v-divider {
  border: 1px solid rgb(224, 222, 222);
  margin: -5px 20px;
}
.editor-explorer {
  display: flex;
  flex: 1;
  flex-direction: column;
  height: 100%;
}
</style>