在上一篇里面我搭建了一个Web的工作流管理平台,实现了对工作流的编辑和部署。现在我们继续完善这个工作流平台的功能,增加查看已部署的工作流,对工作流进行启动,查看执行结果等功能。
要查看已部署的工作流的定义,可以调用camunda的GET /process-definition接口,启动工作流需要调用POST /process-definition/{id}/start的接口,查看执行结果需要调用POST /history/variable-instance接口。
为了能在页面上方便的展示已部署的工作流的列表,我用到了datatable.js这个组件,具体用法可以参见官网DataTables | Table plug-in for jQuery。因为我的Web的框架用的是bootstrap v4版本,所以用以下命令安装对应的库。
npm install --save datatables.net-bs4
在webpack.config.js的copyplugin里面增加以下三句:
- { from: 'node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css', to: 'vendor/datatables.net-bs4/assets' },
- { from: 'node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js', to: 'vendor/datatables.net-bs4/assets' },
- { from: 'node_modules/datatables.net/js/jquery.dataTables.min.js', to: 'vendor/datatables.net-bs4/assets' },
新建一个名为definitions.html的页面,内容如下:
- html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <meta name="description" content="">
- <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
- <meta name="generator" content="Hugo 0.101.0">
- <title>Workflow Definitionstitle>
-
-
- <link href="assets/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
- <link rel="stylesheet" href="vendor/datatables.net-bs4/assets/dataTables.bootstrap4.min.css">
-
- <style>
- .bd-placeholder-img {
- font-size: 1.125rem;
- text-anchor: middle;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
-
- @media (min-width: 768px) {
- .bd-placeholder-img-lg {
- font-size: 3.5rem;
- }
- }
- style>
-
-
- <link href="assets/workflow.css" rel="stylesheet">
- head>
- <body>
- <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
- <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">工作流管理平台a>
- <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
- <span class="navbar-toggler-icon">span>
- button>
- <ul class="navbar-nav px-3">
- <li class="nav-item text-nowrap">
- <a class="nav-link" href="#">退出登录a>
- li>
- ul>
- nav>
-
- <div class="container-fluid d-flex h-75">
- <div class="row flex-fill">
- <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
- <div class="sidebar-sticky pt-3">
- <ul class="nav flex-column">
- <li class="nav-item">
- <a class="nav-link active" href="workflow.html">
- <i data-feather="home">i>
- 编辑工作流
- a>
- li>
- <li class="nav-item">
- <a class="nav-link" href="definitions.html">
- <i data-feather="file">i>
- 查看工作流
- a>
- li>
- <li class="nav-item">
- <a class="nav-link" href="#">
- <i data-feather="shopping-cart">i>
- 编辑规则
- a>
- li>
- <li class="nav-item">
- <a class="nav-link" href="#">
- <i data-feather="users">i>
- 查看规则
- a>
- li>
- ul>
- div>
- nav>
-
- <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
- <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
- <h1 class="h2">工作流定义列表h1>
- div>
- <div class="card shadow mb-4">
- <div class="card-body">
- <div class="table-responsive">
- <table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
- <thead>
- <tr>
- <th>idth>
- <th>名字th>
- <th>描述th>
- <th>版本th>
- <th>操作th>
- tr>
- thead>
- <tbody>
- tbody>
- table>
- div>
- div>
- main>
- div>
- div>
-
-
- <div class="modal fade" id="startModal" tabindex="-1" data-backdrop="static" aria-labelledby="exampleModalLabel" aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="exampleModalLabel">启动工作流h5>
- <button type="button" class="close" data-dismiss="modal" aria-label="Close">
- <span aria-hidden="true">×span>
- button>
- div>
- <div class="modal-body">
- <form id="startForm" instanceId="">
- <div id="row_1" class="form-row">
- <div class="col-auto">
- <div class="text-align:center m-auto">
- <button type="button" class="btn-transparent pt-2" onclick="addFormRow('row_1')">
- <i data-feather="plus-circle">i>
- button>
- div>
- div>
- <div class="col">
- <input id="row_1_variable" type="text" class="form-control" placeholder="变量名">
- div>
- <div class="col">
- <input id="row_1_value" type="text" class="form-control" placeholder="数值">
- div>
- <div class="col">
- <select id="row_1_type" class="custom-select">
- <option selected value="String">Stringoption>
- <option value="Integer">Integeroption>
- <option value="Double">Doubleoption>
- <option value="Boolean">Booleanoption>
- select>
- div>
- <div class="col-auto">
- <div class="text-align:center m-auto">
- <button type="button" class="btn-transparent pt-2" onclick="removeFormRow('row_1')">
- <i data-feather="minus-circle">i>
- button>
- div>
- div>
- div>
- form>
- div>
- <div class="modal-footer">
- <button type="button" class="btn btn-primary" onclick="startForm()">启动button>
- <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭button>
- div>
- div>
- div>
- div>
- <script src="assets/jquery/dist/jquery.slim.min.js">script>
- <script src="assets/bootstrap/dist/bootstrap.bundle.min.js">script>
- <script src="vendor/datatables.net-bs4/assets/jquery.dataTables.min.js">script>
- <script src="vendor/datatables.net-bs4/assets/dataTables.bootstrap4.min.js">script>
- <script src="modals.js">script>
- <script src="definitions.bundle.js">script>
- <script src="assets/feather-icons/dist/feather.min.js">script>
- <script>feather.replace()script>
- body>
- html>
这个页面的主要功能是查询现有已部署的工作流的定义,并通过datatable展示出来。datatable的每一行对应一个工作流定义的一个特定版本,用户可以对这个工作流定义进行启动,暂停,激活,删除等操作,也可点击这个工作流定义的ID来查看相关的已执行完的工作流进程的信息。
这个页面的第88到100行定义了过一个datatable,这里只定义了表头,表的内容则是动态查询工作流定义之后再加载。
第108到157行定义了一个modal,这是bootstrap里面的一个对话框组件。当点击工作流定义的启动按钮时,这个modal将显示出来,用户可以添加启动工作流所需要的参数,并进行启动。
之后我们定义一个definitions.js文件,内容如下:
- import axios from 'axios';
- import Keycloak from 'keycloak-js';
- import config from './config.json';
- const feather = require('feather-icons');
-
- async function initKeycloak() {
- const keycloak = new Keycloak();
- await keycloak.init({onLoad: 'login-required'});
- return keycloak.token;
- }
-
- var token;
- var rowNumber = 1;
-
- window.startDefinition = function startDefinition(id) {
- rowNumber = 1;
- $('#startForm').empty();
- $('#startForm').attr("definitionId", id);
- $('#startForm').append(generateFormRowHtml(rowNumber));
- $('#startModal').modal();
- }
-
- window.suspendDefinition = function suspendDefinition(id, flag) {
- var data = {"suspended": flag, "includeProcessInstances": true};
- axios.create({withCredentials: true}).put(
- config.baseurl + '/engine-rest/process-definition/'+id+'/suspended',
- data,
- {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- if (res.status==204) {
- if (flag) {
- alert("工作流进程已暂停");
- }
- else {
- alert("工作流进程已激活");
- }
- }
- else {
- alert("操作失败,故障码为"+res.status.toString());
- }
- }
- )
- }
-
- window.deleteDefinition = function deleteDefinition(id) {
- var flag = confirm("确定要删除这个工作流定义及所有相关的进程吗?");
- if (flag) {
- axios.create({withCredentials: true}).delete(
- config.baseurl + '/engine-rest/process-definition/'+id+'?cascade=true&skipCustomListeners=true&skipIoMappings=true',
- {headers: {'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- if (res.status==200) {
- alert("工作流已删除");
- location.reload();
- }
- else {
- alert("操作失败,故障码为"+res.status.toString());
- }
- }
- )
- }
- }
-
- window.generateFormRowHtml = function generateFormRowHtml(id) {
- var htmlcode = 'toString()+'" class="form-row mt-2">' +
- '' +
- '' +
- '()+'\')">' +
- feather.icons['plus-circle'].toSvg() +
- '' +
- '' +
- '' +
- '' +
- 'toString()+'_variable" type="text" class="form-control" placeholder="变量名">' +
- '' +
- '' +
- 'toString()+'_value" type="text" class="form-control" placeholder="数值">' +
- '' +
- '' +
- '()+'_type" class="custom-select">' +
- '' +
- '' +
- '' +
- ''
- '' +
- '' +
- '' +
- '' +
- '()+'\')">' +
- feather.icons['minus-circle'].toSvg() +
- '' +
- '' +
- '' +
- '';
- return htmlcode;
- }
-
- window.addFormRow = function addFormRow(id) {
- rowNumber += 1;
- $('#startForm').append(generateFormRowHtml(id));
- }
-
- window.removeFormRow = function removeFormRow(id) {
- $('#'+id).remove();
- }
-
- window.startForm = function startForm() {
- var formValues = {"variables":{}};
- for (var i=1;i
1;i++) { - var rowname = 'row_' + i.toString() + '_';
- console.log(rowname+'variable');
- if ($('#'+rowname+'variable')) {
- var varName = $('#'+rowname+'variable').val();
- var varValue = $('#'+rowname+'value').val();
- var varType = $('#'+rowname+'type').val();
- if (varType=='Float') {
- varValue = parseFloat(varValue);
- }
- if (varType=='Integer') {
- varValue = parseInt(varValue);
- }
- if (varType=='Boolean') {
- varValue = Boolean(varValue);
- }
- if (varName && varValue && varType) {
- formValues['variables'][varName] = {
- "value": varValue,
- "type": varType
- }
- }
- }
- }
- var definitionId = $('#startForm').attr('definitionId');
- $('#startModal').modal('hide');
- axios.create({withCredentials: true}).post(
- config.baseurl + '/engine-rest/process-definition/'+definitionId+'/start',
- formValues,
- {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- if (res.status==200) {
- alert("工作流进程启动成功:"+res.data.links[0].href);
- }
- else {
- alert("操作失败,故障码为"+res.status.toString());
- }
- }
- )
- console.log(JSON.stringify(formValues));
- }
-
- $(document).ready(async function () {
- token = await initKeycloak();
- axios.create({withCredentials: true}).get(
- config.baseurl + '/engine-rest/process-definition',
- {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- console.log(res.data);
- var tableData = res.data;
- for (var i=0;i
length;i++) { - tableData[i].operation = '+'\')">'+feather.icons['play'].toSvg()+''+
- '+'\', true)">'+feather.icons['pause'].toSvg()+''+
- '+'\', false)">'+feather.icons['activity'].toSvg()+''+
- '+'\'">'+feather.icons['edit'].toSvg()+''+
- '+'\')">'+feather.icons['trash-2'].toSvg()+'';
- }
- $('#dataTable').DataTable({
- data: tableData,
- columns: [
- {
- data: "id",
- render: function(data) {
- }
- },
- {data: "name"},
- {data: "description"},
- {data: "version"},
- {data: "operation"}
- ]
- });
- }
- )
- });
这个JS程序里面的第156到186行是调用Camunda的接口,查询现在已部署的工作流定义的数据,并动态加载到datatable中。
其他的代码是提供了对工作流定义的操作。其中当点击编辑的按钮时,将跳转到之前的workflow.html页面进行编辑。为此,我们需要对之前的workflow.js代码做一点小的改动,使得可以获取到跳转过来时带的definitionId的参数,并读取camunda的接口,获取这个工作流的XML数据并呈现出来,改动后的workflow.js的内容如下:
- import $ from 'jquery';
- import './workflow.less';
- import BpmnModeler from 'bpmn-js/lib/Modeler';
- import diagramXML from './diagram.bpmn';
- import Keycloak from 'keycloak-js';
- import axios from 'axios';
- import config from './config.json';
-
- import {
- BpmnPropertiesPanelModule,
- BpmnPropertiesProviderModule,
- CamundaPlatformPropertiesProviderModule
- } from 'bpmn-js-properties-panel';
-
- import CamundaBpmnModdle from 'camunda-bpmn-moddle/resources/camunda.json';
- import customTranslate from './customTranslate/customTranslate';
-
- var customTranslateModule = {
- translate: [ 'value', customTranslate ]
- };
-
- var modeler = new BpmnModeler({
- container: '#js-canvas',
- propertiesPanel: {
- parent: '#js-properties-panel'
- },
- additionalModules: [
- BpmnPropertiesPanelModule,
- BpmnPropertiesProviderModule,
- CamundaPlatformPropertiesProviderModule,
- customTranslateModule
- ],
- moddleExtensions: {
- camunda: CamundaBpmnModdle
- }
- });
-
- var container = $('#js-drop-zone');
- var token;
-
- async function initKeycloak() {
- const keycloak = new Keycloak();
- await keycloak.init({onLoad: 'login-required'});
- return keycloak.token;
- }
-
- $(document).ready(async function () {
- token = await initKeycloak();
- console.log(token);
- var str = window.location.search;
- if (str) {
- var parameter = str.split('=');
- var definitionId = parameter[1];
-
- axios.get(
- config.baseurl + '/engine-rest/process-definition/'+definitionId+'/xml',
- {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- if (res.status==200) {
- createNewDiagram(res.data.bpmn20Xml);
- }
- else {
- alert("读取工作流定义失败,故障码为"+res.status.toString());
- }
- }
- );
- }
- });
-
- // Deployment button
- $('#js-deployment').on("click", async function(event){
- const { xml } = await modeler.saveXML({ format: true });
- const parser = new DOMParser();
- const xmldoc = parser.parseFromString(xml, "application/xml");
- const processes = xmldoc.getElementsByTagName('bpmn2:process');
- const process_name = processes[0].getAttribute('name');
- const file = new File([xml], "diagram.bpmn", {type: "text/plain"});
- const { svg } = await modeler.saveSVG();
- const img_file = new File([svg], "diagram.svg", {type: "image/svg+xml"});
- const data = new FormData();
- data.append("deployment-name", process_name);
- data.append("deployment-source", "process application");
- data.append("data", file);
- data.append("diagram", img_file);
- axios.create({withCredentials: true}).post(
- config.baseurl+'/engine-rest/deployment/create',
- data,
- {headers: {'Content-Type':'multipart/form-data', 'Authorization': 'Bearer '+token}}
- ).then(
- res=>{
- if (res.status==200) {
- alert("部署成功,点击链接查看:"+res.data.links[0].href);
- }
- }
- );
- });
-
- function createNewDiagram(xml) {
- openDiagram(xml);
- }
-
- async function openDiagram(xml) {
- try {
- await modeler.importXML(xml);
- container
- .removeClass('with-error')
- .addClass('with-diagram');
- } catch (err) {
- container
- .removeClass('with-diagram')
- .addClass('with-error');
- container.find('.error pre').text(err.message);
- console.error(err);
- }
- }
-
- function registerFileDrop(container, callback) {
- function handleFileSelect(e) {
- e.stopPropagation();
- e.preventDefault();
- var files = e.dataTransfer.files;
- var file = files[0];
- var reader = new FileReader();
- reader.onload = function(e) {
- var xml = e.target.result;
- callback(xml);
- };
- reader.readAsText(file);
- }
-
- function handleDragOver(e) {
- e.stopPropagation();
- e.preventDefault();
- e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
- }
-
- container.get(0).addEventListener('dragover', handleDragOver, false);
- container.get(0).addEventListener('drop', handleFileSelect, false);
- }
-
-
- // file drag / drop ///
- // check file api availability
- if (!window.FileList || !window.FileReader) {
- window.alert(
- 'Looks like you use an older browser that does not support drag and drop. ' +
- 'Try using Chrome, Firefox or the Internet Explorer > 10.');
- } else {
- registerFileDrop(container, openDiagram);
- console.log("registered");
- }
-
- // bootstrap diagram functions
- $(function() {
- $('#js-create-diagram').on('click', function(e) {
- e.stopPropagation();
- e.preventDefault();
- createNewDiagram(diagramXML);
- });
-
- var downloadLink = $('#js-download-diagram');
- var downloadSvgLink = $('#js-download-svg');
-
- $('.buttons a').on('click', function(e) {
- if (!$(this).is('.active')) {
- e.preventDefault();
- e.stopPropagation();
- }
- });
-
- function setEncoded(link, name, data) {
- var encodedData = encodeURIComponent(data);
- if (data) {
- link.addClass('active').attr({
- 'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
- 'download': name
- });
- } else {
- link.removeClass('active');
- }
- }
-
- var exportArtifacts = debounce(async function() {
- try {
- const { svg } = await modeler.saveSVG();
- setEncoded(downloadSvgLink, 'diagram.svg', svg);
- } catch (err) {
- console.error('Error happened saving svg: ', err);
- setEncoded(downloadSvgLink, 'diagram.svg', null);
- }
- try {
- const { xml } = await modeler.saveXML({ format: true });
- setEncoded(downloadLink, 'diagram.bpmn', xml);
- } catch (err) {
- console.error('Error happened saving XML: ', err);
- setEncoded(downloadLink, 'diagram.bpmn', null);
- }
- }, 500);
-
- modeler.on('commandStack.changed', exportArtifacts);
- });
-
- // helpers //
- function debounce(fn, timeout) {
- var timer;
- return function() {
- if (timer) {
- clearTimeout(timer);
- }
- timer = setTimeout(fn, timeout);
- };
- }
运行npm run build编译后,效果如下:
view_workflow_1