• 搭建一个自定义的工作流管理平台(二)


    在上一篇里面我搭建了一个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里面增加以下三句:

    1. { from: 'node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css', to: 'vendor/datatables.net-bs4/assets' },
    2. { from: 'node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js', to: 'vendor/datatables.net-bs4/assets' },
    3. { from: 'node_modules/datatables.net/js/jquery.dataTables.min.js', to: 'vendor/datatables.net-bs4/assets' },

    新建一个名为definitions.html的页面,内容如下:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="utf-8">
    5. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    6. <meta name="description" content="">
    7. <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    8. <meta name="generator" content="Hugo 0.101.0">
    9. <title>Workflow Definitionstitle>
    10. <link href="assets/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
    11. <link rel="stylesheet" href="vendor/datatables.net-bs4/assets/dataTables.bootstrap4.min.css">
    12. <style>
    13. .bd-placeholder-img {
    14. font-size: 1.125rem;
    15. text-anchor: middle;
    16. -webkit-user-select: none;
    17. -moz-user-select: none;
    18. -ms-user-select: none;
    19. user-select: none;
    20. }
    21. @media (min-width: 768px) {
    22. .bd-placeholder-img-lg {
    23. font-size: 3.5rem;
    24. }
    25. }
    26. style>
    27. <link href="assets/workflow.css" rel="stylesheet">
    28. head>
    29. <body>
    30. <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
    31. <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">工作流管理平台a>
    32. <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">
    33. <span class="navbar-toggler-icon">span>
    34. button>
    35. <ul class="navbar-nav px-3">
    36. <li class="nav-item text-nowrap">
    37. <a class="nav-link" href="#">退出登录a>
    38. li>
    39. ul>
    40. nav>
    41. <div class="container-fluid d-flex h-75">
    42. <div class="row flex-fill">
    43. <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
    44. <div class="sidebar-sticky pt-3">
    45. <ul class="nav flex-column">
    46. <li class="nav-item">
    47. <a class="nav-link active" href="workflow.html">
    48. <i data-feather="home">i>
    49. 编辑工作流
    50. a>
    51. li>
    52. <li class="nav-item">
    53. <a class="nav-link" href="definitions.html">
    54. <i data-feather="file">i>
    55. 查看工作流
    56. a>
    57. li>
    58. <li class="nav-item">
    59. <a class="nav-link" href="#">
    60. <i data-feather="shopping-cart">i>
    61. 编辑规则
    62. a>
    63. li>
    64. <li class="nav-item">
    65. <a class="nav-link" href="#">
    66. <i data-feather="users">i>
    67. 查看规则
    68. a>
    69. li>
    70. ul>
    71. div>
    72. nav>
    73. <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
    74. <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
    75. <h1 class="h2">工作流定义列表h1>
    76. div>
    77. <div class="card shadow mb-4">
    78. <div class="card-body">
    79. <div class="table-responsive">
    80. <table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
    81. <thead>
    82. <tr>
    83. <th>idth>
    84. <th>名字th>
    85. <th>描述th>
    86. <th>版本th>
    87. <th>操作th>
    88. tr>
    89. thead>
    90. <tbody>
    91. tbody>
    92. table>
    93. div>
    94. div>
    95. main>
    96. div>
    97. div>
    98. <div class="modal fade" id="startModal" tabindex="-1" data-backdrop="static" aria-labelledby="exampleModalLabel" aria-hidden="true">
    99. <div class="modal-dialog">
    100. <div class="modal-content">
    101. <div class="modal-header">
    102. <h5 class="modal-title" id="exampleModalLabel">启动工作流h5>
    103. <button type="button" class="close" data-dismiss="modal" aria-label="Close">
    104. <span aria-hidden="true">×span>
    105. button>
    106. div>
    107. <div class="modal-body">
    108. <form id="startForm" instanceId="">
    109. <div id="row_1" class="form-row">
    110. <div class="col-auto">
    111. <div class="text-align:center m-auto">
    112. <button type="button" class="btn-transparent pt-2" onclick="addFormRow('row_1')">
    113. <i data-feather="plus-circle">i>
    114. button>
    115. div>
    116. div>
    117. <div class="col">
    118. <input id="row_1_variable" type="text" class="form-control" placeholder="变量名">
    119. div>
    120. <div class="col">
    121. <input id="row_1_value" type="text" class="form-control" placeholder="数值">
    122. div>
    123. <div class="col">
    124. <select id="row_1_type" class="custom-select">
    125. <option selected value="String">Stringoption>
    126. <option value="Integer">Integeroption>
    127. <option value="Double">Doubleoption>
    128. <option value="Boolean">Booleanoption>
    129. select>
    130. div>
    131. <div class="col-auto">
    132. <div class="text-align:center m-auto">
    133. <button type="button" class="btn-transparent pt-2" onclick="removeFormRow('row_1')">
    134. <i data-feather="minus-circle">i>
    135. button>
    136. div>
    137. div>
    138. div>
    139. form>
    140. div>
    141. <div class="modal-footer">
    142. <button type="button" class="btn btn-primary" onclick="startForm()">启动button>
    143. <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭button>
    144. div>
    145. div>
    146. div>
    147. div>
    148. <script src="assets/jquery/dist/jquery.slim.min.js">script>
    149. <script src="assets/bootstrap/dist/bootstrap.bundle.min.js">script>
    150. <script src="vendor/datatables.net-bs4/assets/jquery.dataTables.min.js">script>
    151. <script src="vendor/datatables.net-bs4/assets/dataTables.bootstrap4.min.js">script>
    152. <script src="modals.js">script>
    153. <script src="definitions.bundle.js">script>
    154. <script src="assets/feather-icons/dist/feather.min.js">script>
    155. <script>feather.replace()script>
    156. body>
    157. html>

    这个页面的主要功能是查询现有已部署的工作流的定义,并通过datatable展示出来。datatable的每一行对应一个工作流定义的一个特定版本,用户可以对这个工作流定义进行启动,暂停,激活,删除等操作,也可点击这个工作流定义的ID来查看相关的已执行完的工作流进程的信息。

    这个页面的第88到100行定义了过一个datatable,这里只定义了表头,表的内容则是动态查询工作流定义之后再加载。

    第108到157行定义了一个modal,这是bootstrap里面的一个对话框组件。当点击工作流定义的启动按钮时,这个modal将显示出来,用户可以添加启动工作流所需要的参数,并进行启动。

    之后我们定义一个definitions.js文件,内容如下:

    1. import axios from 'axios';
    2. import Keycloak from 'keycloak-js';
    3. import config from './config.json';
    4. const feather = require('feather-icons');
    5. async function initKeycloak() {
    6. const keycloak = new Keycloak();
    7. await keycloak.init({onLoad: 'login-required'});
    8. return keycloak.token;
    9. }
    10. var token;
    11. var rowNumber = 1;
    12. window.startDefinition = function startDefinition(id) {
    13. rowNumber = 1;
    14. $('#startForm').empty();
    15. $('#startForm').attr("definitionId", id);
    16. $('#startForm').append(generateFormRowHtml(rowNumber));
    17. $('#startModal').modal();
    18. }
    19. window.suspendDefinition = function suspendDefinition(id, flag) {
    20. var data = {"suspended": flag, "includeProcessInstances": true};
    21. axios.create({withCredentials: true}).put(
    22. config.baseurl + '/engine-rest/process-definition/'+id+'/suspended',
    23. data,
    24. {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
    25. ).then(
    26. res=>{
    27. if (res.status==204) {
    28. if (flag) {
    29. alert("工作流进程已暂停");
    30. }
    31. else {
    32. alert("工作流进程已激活");
    33. }
    34. }
    35. else {
    36. alert("操作失败,故障码为"+res.status.toString());
    37. }
    38. }
    39. )
    40. }
    41. window.deleteDefinition = function deleteDefinition(id) {
    42. var flag = confirm("确定要删除这个工作流定义及所有相关的进程吗?");
    43. if (flag) {
    44. axios.create({withCredentials: true}).delete(
    45. config.baseurl + '/engine-rest/process-definition/'+id+'?cascade=true&skipCustomListeners=true&skipIoMappings=true',
    46. {headers: {'Authorization': 'Bearer '+token}}
    47. ).then(
    48. res=>{
    49. if (res.status==200) {
    50. alert("工作流已删除");
    51. location.reload();
    52. }
    53. else {
    54. alert("操作失败,故障码为"+res.status.toString());
    55. }
    56. }
    57. )
    58. }
    59. }
    60. window.generateFormRowHtml = function generateFormRowHtml(id) {
    61. var htmlcode = '
      toString()+'" class="form-row mt-2">' +
    62. '
      ' +
    63. '
      ' +
    64. '
    65. feather.icons['plus-circle'].toSvg() +
    66. '' +
    67. '
      ' +
  • '
    ' +
  • '
    ' +
  • 'toString()+'_variable" type="text" class="form-control" placeholder="变量名">' +
  • '
    ' +
  • '
    ' +
  • 'toString()+'_value" type="text" class="form-control" placeholder="数值">' +
  • '
    ' +
  • '
    ' +
  • '' +
  • '
    ' +
  • '
    ' +
  • '
    ' +
  • '
  • 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;i1;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;ilength;i++) {
  • tableData[i].operation = ''+
  • ''+
  • ''+
  • ''+
  • '';
  • }
  • $('#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的内容如下:

    1. import $ from 'jquery';
    2. import './workflow.less';
    3. import BpmnModeler from 'bpmn-js/lib/Modeler';
    4. import diagramXML from './diagram.bpmn';
    5. import Keycloak from 'keycloak-js';
    6. import axios from 'axios';
    7. import config from './config.json';
    8. import {
    9. BpmnPropertiesPanelModule,
    10. BpmnPropertiesProviderModule,
    11. CamundaPlatformPropertiesProviderModule
    12. } from 'bpmn-js-properties-panel';
    13. import CamundaBpmnModdle from 'camunda-bpmn-moddle/resources/camunda.json';
    14. import customTranslate from './customTranslate/customTranslate';
    15. var customTranslateModule = {
    16. translate: [ 'value', customTranslate ]
    17. };
    18. var modeler = new BpmnModeler({
    19. container: '#js-canvas',
    20. propertiesPanel: {
    21. parent: '#js-properties-panel'
    22. },
    23. additionalModules: [
    24. BpmnPropertiesPanelModule,
    25. BpmnPropertiesProviderModule,
    26. CamundaPlatformPropertiesProviderModule,
    27. customTranslateModule
    28. ],
    29. moddleExtensions: {
    30. camunda: CamundaBpmnModdle
    31. }
    32. });
    33. var container = $('#js-drop-zone');
    34. var token;
    35. async function initKeycloak() {
    36. const keycloak = new Keycloak();
    37. await keycloak.init({onLoad: 'login-required'});
    38. return keycloak.token;
    39. }
    40. $(document).ready(async function () {
    41. token = await initKeycloak();
    42. console.log(token);
    43. var str = window.location.search;
    44. if (str) {
    45. var parameter = str.split('=');
    46. var definitionId = parameter[1];
    47. axios.get(
    48. config.baseurl + '/engine-rest/process-definition/'+definitionId+'/xml',
    49. {headers: {'Content-Type':'application/json', 'Authorization': 'Bearer '+token}}
    50. ).then(
    51. res=>{
    52. if (res.status==200) {
    53. createNewDiagram(res.data.bpmn20Xml);
    54. }
    55. else {
    56. alert("读取工作流定义失败,故障码为"+res.status.toString());
    57. }
    58. }
    59. );
    60. }
    61. });
    62. // Deployment button
    63. $('#js-deployment').on("click", async function(event){
    64. const { xml } = await modeler.saveXML({ format: true });
    65. const parser = new DOMParser();
    66. const xmldoc = parser.parseFromString(xml, "application/xml");
    67. const processes = xmldoc.getElementsByTagName('bpmn2:process');
    68. const process_name = processes[0].getAttribute('name');
    69. const file = new File([xml], "diagram.bpmn", {type: "text/plain"});
    70. const { svg } = await modeler.saveSVG();
    71. const img_file = new File([svg], "diagram.svg", {type: "image/svg+xml"});
    72. const data = new FormData();
    73. data.append("deployment-name", process_name);
    74. data.append("deployment-source", "process application");
    75. data.append("data", file);
    76. data.append("diagram", img_file);
    77. axios.create({withCredentials: true}).post(
    78. config.baseurl+'/engine-rest/deployment/create',
    79. data,
    80. {headers: {'Content-Type':'multipart/form-data', 'Authorization': 'Bearer '+token}}
    81. ).then(
    82. res=>{
    83. if (res.status==200) {
    84. alert("部署成功,点击链接查看:"+res.data.links[0].href);
    85. }
    86. }
    87. );
    88. });
    89. function createNewDiagram(xml) {
    90. openDiagram(xml);
    91. }
    92. async function openDiagram(xml) {
    93. try {
    94. await modeler.importXML(xml);
    95. container
    96. .removeClass('with-error')
    97. .addClass('with-diagram');
    98. } catch (err) {
    99. container
    100. .removeClass('with-diagram')
    101. .addClass('with-error');
    102. container.find('.error pre').text(err.message);
    103. console.error(err);
    104. }
    105. }
    106. function registerFileDrop(container, callback) {
    107. function handleFileSelect(e) {
    108. e.stopPropagation();
    109. e.preventDefault();
    110. var files = e.dataTransfer.files;
    111. var file = files[0];
    112. var reader = new FileReader();
    113. reader.onload = function(e) {
    114. var xml = e.target.result;
    115. callback(xml);
    116. };
    117. reader.readAsText(file);
    118. }
    119. function handleDragOver(e) {
    120. e.stopPropagation();
    121. e.preventDefault();
    122. e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
    123. }
    124. container.get(0).addEventListener('dragover', handleDragOver, false);
    125. container.get(0).addEventListener('drop', handleFileSelect, false);
    126. }
    127. // file drag / drop ///
    128. // check file api availability
    129. if (!window.FileList || !window.FileReader) {
    130. window.alert(
    131. 'Looks like you use an older browser that does not support drag and drop. ' +
    132. 'Try using Chrome, Firefox or the Internet Explorer > 10.');
    133. } else {
    134. registerFileDrop(container, openDiagram);
    135. console.log("registered");
    136. }
    137. // bootstrap diagram functions
    138. $(function() {
    139. $('#js-create-diagram').on('click', function(e) {
    140. e.stopPropagation();
    141. e.preventDefault();
    142. createNewDiagram(diagramXML);
    143. });
    144. var downloadLink = $('#js-download-diagram');
    145. var downloadSvgLink = $('#js-download-svg');
    146. $('.buttons a').on('click', function(e) {
    147. if (!$(this).is('.active')) {
    148. e.preventDefault();
    149. e.stopPropagation();
    150. }
    151. });
    152. function setEncoded(link, name, data) {
    153. var encodedData = encodeURIComponent(data);
    154. if (data) {
    155. link.addClass('active').attr({
    156. 'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
    157. 'download': name
    158. });
    159. } else {
    160. link.removeClass('active');
    161. }
    162. }
    163. var exportArtifacts = debounce(async function() {
    164. try {
    165. const { svg } = await modeler.saveSVG();
    166. setEncoded(downloadSvgLink, 'diagram.svg', svg);
    167. } catch (err) {
    168. console.error('Error happened saving svg: ', err);
    169. setEncoded(downloadSvgLink, 'diagram.svg', null);
    170. }
    171. try {
    172. const { xml } = await modeler.saveXML({ format: true });
    173. setEncoded(downloadLink, 'diagram.bpmn', xml);
    174. } catch (err) {
    175. console.error('Error happened saving XML: ', err);
    176. setEncoded(downloadLink, 'diagram.bpmn', null);
    177. }
    178. }, 500);
    179. modeler.on('commandStack.changed', exportArtifacts);
    180. });
    181. // helpers //
    182. function debounce(fn, timeout) {
    183. var timer;
    184. return function() {
    185. if (timer) {
    186. clearTimeout(timer);
    187. }
    188. timer = setTimeout(fn, timeout);
    189. };
    190. }

    运行npm run build编译后,效果如下:

    view_workflow_1

  • 相关阅读:
    【无标题】
    pytorch的安装【全官网流程】
    【C进阶】之指针函数和函数指针
    【软考】文件的组织结构
    前端开发神器之 VsCode AI 辅助插件 DevChat
    (十三)admin-boot项目之redis注解实现接口限流
    如何选择线程数量
    一分钟让你学会如何合并PDF文件
    基于蚁群结合遗传算法的路径规划问题附Matlab代码
    【Skynet 入门实战练习】开发环境搭建 | 运行第一个项目 | debug console 简单使用
  • 原文地址:https://blog.csdn.net/gzroy/article/details/127527456