├── packages ├── app │ ├── .eslintignore │ ├── index.js │ ├── bin │ │ └── camunda-playground │ ├── test │ │ ├── .eslintrc │ │ └── util.test.js │ ├── .gitignore │ ├── docs │ │ └── screenshot.png │ ├── .eslintrc │ ├── lib │ │ ├── util │ │ │ └── index.js │ │ ├── cli.js │ │ ├── engine-api.js │ │ └── index.js │ ├── README.md │ ├── package.json │ ├── CHANGELOG.md │ └── diagram.bpmn └── client │ ├── .gitignore │ ├── .eslintrc │ ├── README.md │ ├── public │ ├── animated-logo-grey-48.gif │ └── index.html │ ├── src │ ├── main.js │ ├── components │ │ ├── util │ │ │ └── index.js │ │ ├── viewer │ │ │ ├── features │ │ │ │ ├── FitViewport.js │ │ │ │ └── ProcessInstance.js │ │ │ └── Viewer.js │ │ ├── Loader.svelte │ │ ├── FileDrop.svelte │ │ └── Diagram.svelte │ ├── style │ │ ├── global.scss │ │ └── shared.scss │ └── Client.svelte │ ├── package.json │ ├── rollup.config.js │ └── svelte.config.js ├── .gitignore ├── lerna.json ├── .github └── workflows │ └── CI.yml ├── LICENSE ├── README.md └── package.json /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | static -------------------------------------------------------------------------------- /packages/client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .run-camunda 2 | node_modules -------------------------------------------------------------------------------- /packages/app/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib'); -------------------------------------------------------------------------------- /packages/app/bin/camunda-playground: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli'); -------------------------------------------------------------------------------- /packages/app/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:bpmn-io/mocha" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:bpmn-io/browser" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .run-camunda 3 | tmp 4 | npm-debug.log 5 | coverage 6 | static -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.7.1", 6 | "useWorkspaces": true 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikku/camunda-playground/HEAD/packages/app/docs/screenshot.png -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # camunda-playground-client 2 | 3 | The [Camunda Playground](https://github.com/nikku/camunda-playground) client app. -------------------------------------------------------------------------------- /packages/app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:bpmn-io/node" 4 | ], 5 | "rules": { 6 | "require-atomic-updates": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /packages/client/public/animated-logo-grey-48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikku/camunda-playground/HEAD/packages/client/public/animated-logo-grey-48.gif -------------------------------------------------------------------------------- /packages/client/src/main.js: -------------------------------------------------------------------------------- 1 | import Client from './Client.svelte'; 2 | 3 | const client = new Client({ 4 | target: document.body 5 | }); 6 | 7 | export default client; -------------------------------------------------------------------------------- /packages/client/src/components/util/index.js: -------------------------------------------------------------------------------- 1 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | export function getExternalTaskTopic(activity) { 4 | const bo = getBusinessObject(activity); 5 | 6 | return bo.get('camunda:topic'); 7 | } -------------------------------------------------------------------------------- /packages/app/test/util.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | expect 3 | } = require('chai'); 4 | 5 | 6 | describe('camunda-playground', function() { 7 | 8 | it('should true be true?', function() { 9 | 10 | expect(true).to.be.true; 11 | 12 | }); 13 | 14 | }); -------------------------------------------------------------------------------- /packages/client/src/components/viewer/features/FitViewport.js: -------------------------------------------------------------------------------- 1 | export default class FitViewport { 2 | constructor(canvas, eventBus) { 3 | eventBus.on('import.done', () => { 4 | canvas.zoom('fit-viewport'); 5 | }); 6 | } 7 | } 8 | 9 | FitViewport.$inject = [ 10 | 'canvas', 11 | 'eventBus' 12 | ]; -------------------------------------------------------------------------------- /packages/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Camunda Playground 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | Build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest ] 9 | node-version: [ 20 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'npm' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build 24 | run: npm run all -------------------------------------------------------------------------------- /packages/app/lib/util/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | var crypto = require('crypto'); 5 | 6 | function readFile(filePath) { 7 | 8 | const contents = fs.readFileSync(filePath, 'utf8'); 9 | 10 | const stats = fs.statSync(filePath); 11 | 12 | return { 13 | path: filePath, 14 | name: path.basename(filePath), 15 | contents, 16 | mtimeMs: stats.mtimeMs 17 | }; 18 | } 19 | 20 | module.exports.readFile = readFile; 21 | 22 | 23 | function hash(str) { 24 | return crypto.createHash('md5').update(str).digest('hex'); 25 | } 26 | 27 | module.exports.hash = hash; -------------------------------------------------------------------------------- /packages/client/src/components/Loader.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 |
26 | 27 |
-------------------------------------------------------------------------------- /packages/client/src/components/viewer/Viewer.js: -------------------------------------------------------------------------------- 1 | import NavigatedViewer from 'bpmn-js/dist/bpmn-navigated-viewer.development.js'; 2 | 3 | import FitViewport from './features/FitViewport'; 4 | import ProcessInstance from './features/ProcessInstance'; 5 | 6 | const additionalModules = [ 7 | { 8 | __init__: [ 'fitViewport', 'processInstance' ], 9 | fitViewport: [ 'type', FitViewport ], 10 | processInstance: [ 'type', ProcessInstance ] 11 | } 12 | ]; 13 | 14 | class Viewer extends NavigatedViewer { 15 | constructor(options = {}) { 16 | super({ 17 | ...options, 18 | additionalModules 19 | }); 20 | } 21 | 22 | showProcessInstance(processInstance) { 23 | this.get('processInstance').show(processInstance); 24 | } 25 | 26 | clearProcessInstance() { 27 | this.get('processInstance').clear(); 28 | } 29 | } 30 | 31 | export default Viewer; -------------------------------------------------------------------------------- /packages/client/src/style/global.scss: -------------------------------------------------------------------------------- 1 | @import "./shared.scss"; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | margin: 0; 7 | overflow: hidden; 8 | padding: 0; 9 | } 10 | 11 | .icon { 12 | vertical-align: text-bottom; 13 | } 14 | 15 | .element-overlay { 16 | @include camunda-link; 17 | 18 | animation: jump 1s infinite; 19 | animation-timing-function: ease; 20 | position: relative; 21 | transform: scale(1); 22 | transition: all 0.2 ease-in-out; 23 | 24 | width: auto; 25 | max-width: 300px; 26 | 27 | &:hover { 28 | animation-name: none; 29 | } 30 | 31 | a { 32 | color: white; 33 | } 34 | 35 | code { 36 | border: solid 1px white; 37 | display: inline-block; 38 | padding: .1em .3em; 39 | margin: auto -.1em; 40 | border-radius: 3px; 41 | } 42 | 43 | .long { 44 | flex: 1; 45 | } 46 | 47 | .long.note { 48 | width: 200px; 49 | white-space: normal; 50 | } 51 | } 52 | 53 | @keyframes jump { 54 | 50% { 55 | transform: scale(1.1); 56 | } 57 | } -------------------------------------------------------------------------------- /packages/client/src/style/shared.scss: -------------------------------------------------------------------------------- 1 | @mixin button($color, $fill: true, $backgroundColor: scale-color($color, $lightness: +10%)) { 2 | 3 | border: solid 1px $color; 4 | border-radius: 3px; 5 | 6 | padding: 6px 12px; 7 | font: 14px monospace; 8 | text-decoration: none; 9 | display: inline-block; 10 | white-space: nowrap; 11 | 12 | @if lightness($color) < 50% { 13 | color: white; 14 | } @else { 15 | color: black; 16 | } 17 | 18 | @if $fill { 19 | background: $backgroundColor; 20 | } 21 | 22 | &:hover { 23 | background: $color; 24 | } 25 | } 26 | 27 | @mixin camunda-link { 28 | @include text-container; 29 | @include button(#a40e20); 30 | 31 | & { 32 | border-radius: 16px; 33 | box-sizing: border-box; 34 | 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: start; 38 | } 39 | 40 | .long { 41 | display: none; 42 | flex: 1; 43 | margin-left: .5em; 44 | } 45 | 46 | &:hover .long { 47 | display: initial; 48 | } 49 | } 50 | 51 | @mixin text-container { 52 | line-height: 1.5; 53 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present Nico Rehwaldt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camunda Playground 2 | 3 | [![CI](https://github.com/nikku/camunda-playground/workflows/CI/badge.svg)](https://github.com/nikku/camunda-playground/actions?query=workflow%3ACI) 4 | 5 | Try out and explore [Camunda](https://camunda.com/) in minutes, not hours. 6 | 7 | ![Camunda Playground in action](https://raw.githubusercontent.com/nikku/camunda-playground/master/packages/app/docs/screenshot.png) 8 | 9 | 10 | ## Usage 11 | 12 | If you have [Node.js](https://nodejs.org/) installed, deploy and run your process: 13 | 14 | ``` 15 | npx camunda-playground diagram.bpmn 16 | ``` 17 | 18 | Alternatively, install the `camunda-playground` executable globally: 19 | 20 | ``` 21 | npm install -g camunda-playground 22 | ``` 23 | 24 | 25 | ## Resources 26 | 27 | * [Issues](https://github.com/nikku/camunda-playground/issues) 28 | * [Changelog](./packages/app/CHANGELOG.md) 29 | 30 | 31 | ## Related 32 | 33 | * [run-camunda](https://github.com/nikku/run-camunda) - Download and spin up Camunda painlessly from Node.js 34 | * [Camunda Modeler](https://github.com/camunda/camunda-modeler) - The Camunda modeling and workflow implementation app 35 | 36 | 37 | ## License 38 | 39 | MIT -------------------------------------------------------------------------------- /packages/app/README.md: -------------------------------------------------------------------------------- 1 | # Camunda Playground 2 | 3 | [![CI](https://github.com/nikku/camunda-playground/workflows/CI/badge.svg)](https://github.com/nikku/camunda-playground/actions?query=workflow%3ACI) 4 | 5 | Try out and explore [Camunda](https://camunda.com/) in minutes, not hours. 6 | 7 | ![Camunda Playground in action](https://raw.githubusercontent.com/nikku/camunda-playground/master/packages/app/docs/screenshot.png) 8 | 9 | 10 | ## Usage 11 | 12 | If you have [Node.js](https://nodejs.org/) installed, deploy and run your process: 13 | 14 | ``` 15 | npx camunda-playground diagram.bpmn 16 | ``` 17 | 18 | Alternatively, install the `camunda-playground` executable globally: 19 | 20 | ``` 21 | npm install -g camunda-playground 22 | ``` 23 | 24 | 25 | ## Resources 26 | 27 | * [Issues](https://github.com/nikku/camunda-playground/issues) 28 | * [Changelog](./CHANGELOG.md) 29 | 30 | 31 | ## Related 32 | 33 | * [run-camunda](https://github.com/nikku/run-camunda) - Download and spin up Camunda painlessly from Node.js 34 | * [Camunda Modeler](https://github.com/camunda/camunda-modeler) - The Camunda modeling and workflow implementation app 35 | 36 | 37 | ## License 38 | 39 | MIT -------------------------------------------------------------------------------- /packages/client/src/components/FileDrop.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 48 | 49 |
52 | 53 |
-------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camunda-playground-client", 3 | "version": "0.7.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@rollup/plugin-commonjs": "^28.0.2", 7 | "@rollup/plugin-json": "^6.1.0", 8 | "@rollup/plugin-node-resolve": "^16.0.0", 9 | "@rollup/plugin-terser": "^0.4.4", 10 | "@rollup/plugin-url": "^8.0.2", 11 | "bpmn-js": "^13.1.0", 12 | "eslint-plugin-import": "^2.23.4", 13 | "eslint-plugin-svelte3": "^3.4.1", 14 | "file-drops": "^0.4.0", 15 | "min-dash": "^4.1.1", 16 | "npm-run-all": "^4.1.5", 17 | "rollup": "^4.32.1", 18 | "rollup-plugin-copy": "^3.5.0", 19 | "rollup-plugin-css-only": "^3.1.0", 20 | "rollup-plugin-livereload": "^2.0.5", 21 | "rollup-plugin-svelte": "^7.2.2", 22 | "sass": "1.77.4", 23 | "svelte": "^3.38.3", 24 | "svelte-preprocess": "^5.1.4" 25 | }, 26 | "scripts": { 27 | "test": "echo \"no tests\"", 28 | "lint": "eslint .", 29 | "build": "run-p build:*:prod", 30 | "build:watch": "run-p build:*:watch", 31 | "build:js:prod": "rollup -c", 32 | "build:js:watch": "rollup -c -w", 33 | "build:css": "sass --load-path=../../node_modules src/style/global.scss:../app/static/global.css", 34 | "build:css:prod": "run-s \"build:css -- --style compressed\"", 35 | "build:css:watch": "run-s build:css \"build:css -- -w\"", 36 | "dev": "run-s build:watch" 37 | }, 38 | "dependencies": { 39 | "camunda-bpmn-moddle": "^7.0.1", 40 | "min-dom": "^4.1.0", 41 | "svg-curves": "^1.0.0", 42 | "tiny-svg": "^3.0.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/client/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path'); 4 | 5 | const resolve = require('@rollup/plugin-node-resolve'); 6 | const commonjs = require('@rollup/plugin-commonjs'); 7 | const json = require('@rollup/plugin-json'); 8 | const terser = require('@rollup/plugin-terser'); 9 | const url = require('@rollup/plugin-url'); 10 | 11 | const svelte = require('rollup-plugin-svelte'); 12 | const livereload = require('rollup-plugin-livereload'); 13 | const copy = require('rollup-plugin-copy'); 14 | const css = require('rollup-plugin-css-only'); 15 | 16 | const svelteConfig = require('./svelte.config.js'); 17 | 18 | const distDir = path.resolve(__dirname + '/../app/static'); 19 | 20 | 21 | const production = !process.env.ROLLUP_WATCH; 22 | 23 | module.exports = { 24 | input: 'src/main.js', 25 | output: { 26 | sourcemap: !production, 27 | format: 'iife', 28 | name: 'app', 29 | file: `${distDir}/bundle.js` 30 | }, 31 | plugins: [ 32 | url({ 33 | limit: 3 * 1024 34 | }), 35 | svelte({ 36 | 37 | compilerOptions: { 38 | 39 | // enable run-time checks during development 40 | dev: !production, 41 | 42 | immutable: true 43 | }, 44 | 45 | preprocess: svelteConfig.preprocess 46 | }), 47 | 48 | resolve(), 49 | commonjs(), 50 | json(), 51 | 52 | css({ 53 | output: 'bundle.css' 54 | }), 55 | 56 | !production && livereload(distDir), 57 | 58 | copy({ 59 | targets: [ 60 | { src: 'public/*', dest: distDir } 61 | ] 62 | }), 63 | 64 | production && terser() 65 | ], 66 | watch: { 67 | clearScreen: false 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camunda-playground", 3 | "version": "0.7.1", 4 | "description": "Run your diagrams on Camunda and get to know tools in the stack", 5 | "author": { 6 | "name": "Nico Rehwaldt", 7 | "url": "https://github.com/nikku" 8 | }, 9 | "bin": { 10 | "camunda-playground": "./bin/camunda-playground" 11 | }, 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/nikku/camunda-playground.git", 16 | "directory": "packages/app" 17 | }, 18 | "keywords": [ 19 | "BPM", 20 | "BPMN", 21 | "Camunda", 22 | "Engine" 23 | ], 24 | "scripts": { 25 | "dev": "nodemon", 26 | "start": "node ./bin/camunda-playground", 27 | "test": "mocha test --exit", 28 | "lint": "eslint .", 29 | "auto-test": "npm test -- --watch" 30 | }, 31 | "dependencies": { 32 | "body": "^5.1.0", 33 | "form-data": "^4.0.0", 34 | "get-port": "^5.1.1", 35 | "min-dash": "^3.7.0", 36 | "mri": "^1.1.6", 37 | "node-fetch": "^2.6.1", 38 | "open": "^8.2.1", 39 | "polka": "^0.5.2", 40 | "run-camunda": "^8.0.0", 41 | "sirv": "^1.0.12" 42 | }, 43 | "devDependencies": { 44 | "chai": "^4.3.4", 45 | "mocha": "^9.0.1", 46 | "nodemon": "^2.0.9", 47 | "npm-run-all": "^4.1.5" 48 | }, 49 | "nodemonConfig": { 50 | "exec": "npm start -- --verbose --no-open diagram.bpmn ", 51 | "watch": [ 52 | ".env", 53 | "." 54 | ], 55 | "ignore": [ 56 | ".run-camunda/*", 57 | "static/*", 58 | "tmp/*", 59 | "test/*" 60 | ] 61 | }, 62 | "files": [ 63 | "bin", 64 | "lib", 65 | "static", 66 | "index.js" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camunda-playground-builder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "The Camunda playground builder", 6 | "scripts": { 7 | "clean": "del-cli app/static", 8 | "all": "run-s lint test build", 9 | "lint": "run-s lint:*", 10 | "lint:app": "cd packages/app && npm run lint", 11 | "lint:client": "cd packages/client && npm run lint", 12 | "dev:app": "cd packages/app && npm run dev", 13 | "dev:client": "cd packages/client && npm run dev", 14 | "dev": "NODE_ENV=development run-p dev:* -l", 15 | "build:client": "cd packages/client && npm run build", 16 | "build": "NODE_ENV=production run-s build:*", 17 | "test:app": "cd packages/app && npm test", 18 | "test:client": "cd packages/client && npm test", 19 | "test": "NODE_ENV=test run-s test:*", 20 | "auto-test:app": "cd packages/app && npm run auto-test", 21 | "auto-test": "NODE_ENV=test run-s auto-test:*", 22 | "start": "cd packages/app && npm run start", 23 | "postinstall": "NODE_ENV=development lerna bootstrap", 24 | "release": "npm run clean && npm run all && lerna publish" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/nikku/camunda-playground.git" 29 | }, 30 | "keywords": [ 31 | "BPM", 32 | "BPMN", 33 | "Camunda", 34 | "Engine", 35 | "Executor" 36 | ], 37 | "author": "Nico Rehwaldt", 38 | "license": "MIT", 39 | "devDependencies": { 40 | "del-cli": "^4.0.0", 41 | "eslint": "^8.41.0", 42 | "eslint-plugin-bpmn-io": "^1.0.0", 43 | "lerna": "^6.6.2", 44 | "npm-run-all": "^4.1.5" 45 | }, 46 | "workspaces": [ 47 | "packages/app", 48 | "packages/client" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /packages/app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [camunda-playground](https://github.com/nikku/camunda-playground) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 0.7.1 10 | 11 | * `DOCS`: update screenshot 12 | 13 | ## 0.7.0 14 | 15 | * `FEAT`: improve playground layout 16 | 17 | ## 0.6.0 18 | 19 | * `FEAT`: hint on external task implementation 20 | * `DEPS`: bump bpmn-js and friends 21 | 22 | ## 0.5.0 23 | 24 | * `FEAT`: play using `Camunda 7.19` per default 25 | * `FEAT`: default to Camunda Desktop Modeler as external editor 26 | * `FIX`: correct opening of external apps 27 | 28 | ## 0.4.0 29 | 30 | * `DEPS`: bump dependencies 31 | * `CHORE`: download and run Camunda 7.15 per default 32 | 33 | ## 0.3.1 34 | 35 | * `DEPS`: bump dependencies 36 | * `CHORE`: download and run Camunda 7.12 per default 37 | 38 | ## 0.3.0 39 | 40 | * `CHORE`: further reduce download size 41 | 42 | ## 0.2.0 43 | 44 | * `CHORE`: further reduce download size 45 | 46 | ## 0.1.1 47 | 48 | * `CHORE`: reduce download size 49 | * `FIX`: hide loader after diagram drop 50 | 51 | ## 0.1.0 52 | 53 | * `FEAT`: download and run Camunda 7.11 per default 54 | * `FEAT`: add link to Camunda Cockpit 55 | * `FEAT`: give links to Camunda a unique color 56 | * `FEAT`: provide descriptive titles to Camunda links 57 | 58 | ## 0.0.8 59 | 60 | * `FIX`: betta screenshot 61 | 62 | ## 0.0.7 63 | 64 | * `CHORE`: optimize diagram loading 65 | 66 | ## 0.0.6 67 | 68 | * `FEAT`: display no diagram hint 69 | * `FIX`: make diagram upload not fail if deployment failed 70 | * `DOCS`: add fancy screenshots and run instructions 71 | 72 | ## 0.0.5 73 | 74 | * `FEAT`: display summary 75 | * `FIX`: uploaded diagram not deploying 76 | 77 | ## 0.0.4 78 | 79 | * `FIX`: correct executable 80 | 81 | ## 0.0.3 82 | 83 | * `FEAT`: restart app 84 | * `FEAT`: link tasklist 85 | * `FEAT`: handle deploy errors 86 | 87 | ## 0.0.2 88 | 89 | * `FEAT`: improve responsiveness 90 | * `FEAT`: reload client on external diagram changes 91 | * `FEAT`: show trace on client 92 | 93 | ## 0.0.1 94 | 95 | _Initial version._ 96 | -------------------------------------------------------------------------------- /packages/client/svelte.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const sveltePreprocess = require('svelte-preprocess'); 4 | 5 | const path = require('path'); 6 | 7 | function scriptProcessor(processors) { 8 | 9 | return function(options) { 10 | 11 | const { 12 | content, 13 | ...rest 14 | } = options; 15 | 16 | const code = processors.reduce((content, processor) => { 17 | return processor({ 18 | content, 19 | ...rest 20 | }).code; 21 | }, content); 22 | 23 | return { 24 | code 25 | }; 26 | }; 27 | } 28 | 29 | function classProcessor() { 30 | 31 | function process(content) { 32 | return ( 33 | content 34 | .replace( 35 | /export let className([;\n= ]{1})/g, 36 | 'export { className as class }; let className$1' 37 | ) 38 | ); 39 | } 40 | 41 | return function(options) { 42 | 43 | const { 44 | content 45 | } = options; 46 | 47 | const code = process(content); 48 | 49 | return { 50 | code 51 | }; 52 | }; 53 | } 54 | 55 | 56 | function emitProcessor() { 57 | 58 | function process(content) { 59 | 60 | if (/\$\$emit\(/.test(content)) { 61 | 62 | content = ` 63 | import { createEventDispatcher } from 'svelte'; 64 | 65 | const __dispatch = createEventDispatcher(); 66 | 67 | ${content}`; 68 | 69 | content = content.replace(/\$\$emit\(/g, '__dispatch('); 70 | } 71 | 72 | return content; 73 | } 74 | 75 | return function(options) { 76 | 77 | const { 78 | content 79 | } = options; 80 | 81 | const code = process(content); 82 | 83 | return { 84 | code 85 | }; 86 | }; 87 | } 88 | 89 | module.exports = { 90 | preprocess: [ 91 | sveltePreprocess({ 92 | scss: { 93 | includePaths: [ 94 | path.join(__dirname, 'src/style'), 95 | path.join(__dirname, '../../node_modules') 96 | ] 97 | } 98 | }), 99 | { 100 | script: scriptProcessor([ 101 | classProcessor(), 102 | emitProcessor() 103 | ]) 104 | } 105 | ] 106 | }; -------------------------------------------------------------------------------- /packages/client/src/components/Diagram.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 101 | 102 |
-------------------------------------------------------------------------------- /packages/app/lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mri = require('mri'); 4 | const path = require('path'); 5 | 6 | const opn = require('open'); 7 | 8 | const { 9 | startCamunda, 10 | isCamundaRunning 11 | } = require('run-camunda'); 12 | 13 | const Playground = require('..'); 14 | 15 | const argv = process.argv.slice(2); 16 | 17 | const options = mri(argv, { 18 | default: { 19 | port: 3301, 20 | open: true, 21 | diagramEditor: getDefaultDiagramEditor(), 22 | verbose: false 23 | }, 24 | alias: { 25 | diagramEditor: 'diagram-editor', 26 | version: 'v' 27 | } 28 | }); 29 | 30 | 31 | if (options.verbose) { 32 | console.log('Parsed options: %o', options); 33 | } 34 | 35 | if (options.version) { 36 | console.log(require('../package').version); 37 | 38 | process.exit(0); 39 | } 40 | 41 | if (options.help) { 42 | console.log(`usage: camunda-playground [...options] [diagram] 43 | 44 | Options: 45 | --diagram-editor tool to edit the diagram in 46 | --[no]-open open in browser 47 | 48 | -v, --version output tool version 49 | --verbose enable verbose logging 50 | --help show help 51 | 52 | 53 | Examples: 54 | $ camunda-playground --diagram-editor=camunda-modeler foo.bpmn 55 | `); 56 | 57 | process.exit(0); 58 | 59 | } 60 | 61 | options.diagram = options._[0]; 62 | 63 | function getDefaultDiagramEditor() { 64 | return ( 65 | process.env.CAMUNDA_PLAYGROUND_DIAGRAM_EDITOR || [ 'camunda-modeler', 'Camunda Modeler' ] 66 | ); 67 | } 68 | 69 | async function run(options) { 70 | 71 | const { 72 | diagram, 73 | diagramEditor, 74 | port, 75 | verbose, 76 | open 77 | } = options; 78 | 79 | const diagramPath = diagram ? path.resolve(diagram) : null; 80 | 81 | const isRunning = await isCamundaRunning(); 82 | 83 | if (isRunning) { 84 | console.log('Using the Camunda instance that is running on http://localhost:8080'); 85 | } else { 86 | console.log('Camunda not running yet, fetching and starting it'); 87 | 88 | await startCamunda(); 89 | } 90 | 91 | const url = await Playground.create({ 92 | diagramEditor, 93 | diagramPath, 94 | verbose, 95 | port 96 | }); 97 | 98 | console.log(`Playground started at ${url}`); 99 | 100 | if (open) { 101 | await opn(url); 102 | } 103 | } 104 | 105 | run(options).catch(err => { 106 | console.error(err); 107 | 108 | process.exit(1); 109 | }); -------------------------------------------------------------------------------- /packages/app/lib/engine-api.js: -------------------------------------------------------------------------------- 1 | const FormData = require('form-data'); 2 | 3 | const fetch = require('node-fetch'); 4 | const path = require('path'); 5 | 6 | 7 | function EngineApi(camundaBaseUrl) { 8 | 9 | const baseUrl = `${camundaBaseUrl}/engine-rest`; 10 | 11 | 12 | async function deployDiagram(diagram) { 13 | 14 | const form = new FormData(); 15 | 16 | form.append('deployment-name', 'camunda-playground-deployment'); 17 | form.append('deployment-source', 'camunda-playground'); 18 | 19 | const diagramName = diagram.path && path.basename(diagram.path) || diagram.name; 20 | 21 | form.append(diagramName, diagram.contents, { 22 | filename: diagramName, 23 | contentType: 'application/xml' 24 | }); 25 | 26 | const response = await fetch(`${baseUrl}/deployment/create`, { 27 | method: 'POST', 28 | headers: { 29 | 'accept': 'application/json' 30 | }, 31 | body: form 32 | }); 33 | 34 | if (response.ok) { 35 | 36 | const { 37 | id, 38 | deployedProcessDefinitions 39 | } = await response.json(); 40 | 41 | return { 42 | id, 43 | deployedProcessDefinitions, 44 | deployedProcessDefinition: Object.values(deployedProcessDefinitions || {})[0] 45 | }; 46 | } 47 | 48 | const details = await response.json(); 49 | 50 | throw responseError('Deployment failed', response, details); 51 | } 52 | 53 | async function startProcessInstance(definition) { 54 | 55 | const response = await fetch(`${baseUrl}/process-definition/${definition.id}/start`, { 56 | method: 'POST', 57 | headers: { 58 | 'accept': 'application/json', 59 | 'content-type': 'application/json' 60 | }, 61 | body: JSON.stringify({}) 62 | }); 63 | 64 | if (response.ok) { 65 | return response.json(); 66 | } 67 | 68 | const details = await response.json(); 69 | 70 | throw responseError('Starting process instance failed', response, details); 71 | } 72 | 73 | async function getProcessInstanceDetails(processInstance) { 74 | 75 | const { 76 | id 77 | } = processInstance; 78 | 79 | const [ 80 | state, 81 | activityInstances 82 | ] = await Promise.all([ 83 | 84 | // https://docs.camunda.org/manual/7.11/reference/rest/history/process-instance/get-process-instance/ 85 | fetch(`${baseUrl}/history/process-instance/${id}`, { 86 | headers: { 87 | 'accept': 'application/json', 88 | 'content-type': 'application/json' 89 | } 90 | }), 91 | 92 | // https://docs.camunda.org/manual/7.11/reference/rest/history/activity-instance/get-activity-instance-query/ 93 | fetch(`${baseUrl}/history/activity-instance?processInstanceId=${id}`, { 94 | headers: { 95 | 'accept': 'application/json', 96 | 'content-type': 'application/json' 97 | } 98 | }) 99 | ].map(result => result.then(res => res.json()))); 100 | 101 | return { 102 | id, 103 | state, 104 | activityInstances 105 | }; 106 | } 107 | 108 | // api ///////////////// 109 | 110 | this.deployDiagram = deployDiagram; 111 | this.getProcessInstanceDetails = getProcessInstanceDetails; 112 | this.startProcessInstance = startProcessInstance; 113 | 114 | } 115 | 116 | 117 | module.exports = EngineApi; 118 | 119 | 120 | // helpers ////////////// 121 | 122 | const parseError = 'ENGINE-09005 Could not parse BPMN process. Errors: \n*'; 123 | 124 | function responseError(message, response, details) { 125 | const error = new Error(message); 126 | 127 | error.details = details; 128 | error.response = response; 129 | 130 | // fix engine not exposing details 131 | if (details && details.message.startsWith(parseError)) { 132 | details.problems = details.message.substring(parseError.length).split(/\s?\n\*\s?/g); 133 | details.message = 'ENGINE-09005 Could not parse BPMN process'; 134 | } 135 | 136 | return error; 137 | } -------------------------------------------------------------------------------- /packages/app/diagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_1bv4zgg 6 | 7 | 8 | SequenceFlow_0ruxd7d 9 | 10 | 11 | SequenceFlow_03qo2a7 12 | SequenceFlow_1q390uc 13 | 14 | 15 | SequenceFlow_0t7bwia 16 | SequenceFlow_0qrpyu7 17 | 18 | 19 | SequenceFlow_1bv4zgg 20 | SequenceFlow_0t7bwia 21 | SequenceFlow_03qo2a7 22 | 23 | 24 | SequenceFlow_0qrpyu7 25 | SequenceFlow_1q390uc 26 | SequenceFlow_0ruxd7d 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /packages/client/src/components/viewer/features/ProcessInstance.js: -------------------------------------------------------------------------------- 1 | import { 2 | filter, 3 | find, 4 | forEach, 5 | assign, 6 | isFunction, 7 | map, 8 | matchPattern, 9 | uniqueBy, 10 | values 11 | } from 'min-dash'; 12 | 13 | import { 14 | append as svgAppend, 15 | attr as svgAttr, 16 | clear as svgClear, 17 | create as svgCreate 18 | } from 'tiny-svg'; 19 | 20 | import { 21 | domify, 22 | query as domQuery 23 | } from 'min-dom'; 24 | 25 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 26 | 27 | import { createCurve } from 'svg-curves'; 28 | import { getExternalTaskTopic } from '../../util'; 29 | 30 | const FILL = '#52B415'; 31 | 32 | const OFFSET_TOP = -15, 33 | OFFSET_RIGHT = 15; 34 | 35 | export default class ProcessInstance { 36 | constructor(canvas, elementRegistry, eventBus, overlays) { 37 | this._canvas = canvas; 38 | this._elementRegistry = elementRegistry; 39 | this._overlays = overlays; 40 | 41 | eventBus.on('import.done', () => { 42 | let defs = domQuery('defs', canvas._svg); 43 | 44 | if (!defs) { 45 | defs = svgCreate('defs'); 46 | 47 | svgAppend(canvas._svg, defs); 48 | } 49 | 50 | const marker = svgCreate('marker'); 51 | 52 | svgAttr(marker, { 53 | id: 'arrow', 54 | viewBox: '0 0 10 10', 55 | refX: 7, 56 | refY: 5, 57 | markerWidth: 4, 58 | markerHeight: 4, 59 | orient: 'auto-start-reverse' 60 | }); 61 | 62 | const path = svgCreate('path'); 63 | 64 | svgAttr(path, { 65 | d: 'M 0 0 L 10 5 L 0 10 z', 66 | fill: FILL, 67 | stroke: 'blue', 68 | strokeWidth: 0 69 | }); 70 | 71 | svgAppend(marker, path); 72 | 73 | svgAppend(defs, marker); 74 | }); 75 | } 76 | 77 | show(processInstance) { 78 | const { id: processInstanceId } = processInstance; 79 | 80 | const connections = this._getConnections(processInstance); 81 | 82 | connections.forEach(connection => { 83 | this._addConnectionMarker(connection, { 84 | markerEnd: 'url(#arrow)' 85 | }); 86 | }); 87 | 88 | const dottedConnections = this._getDottedConnections(connections); 89 | 90 | dottedConnections.forEach(connection => { 91 | this._addConnectionMarker(connection, { 92 | strokeDasharray: '1 8', 93 | strokeLinecap: 'round' 94 | }); 95 | }); 96 | 97 | // activities that have NOT ended 98 | const activities = this._getActivities(processInstance, activity => !activity.endTime); 99 | 100 | activities.forEach(activity => { 101 | const taskId = find(processInstance.trace, a => a.activityId === activity.id).taskId; 102 | 103 | this._addActivityButton(activity, taskId, processInstanceId); 104 | }); 105 | } 106 | 107 | _getActivities(processInstance, filterFn) { 108 | const elementRegistry = this._elementRegistry; 109 | 110 | const { trace } = processInstance; 111 | 112 | let activities = values(trace); 113 | 114 | if (isFunction(filterFn)) { 115 | activities = filter(activities, filterFn); 116 | } 117 | 118 | return map(activities, ({ activityId }) => elementRegistry.get(activityId)); 119 | } 120 | 121 | _getConnections(processInstance) { 122 | const activities = this._getActivities(processInstance); 123 | 124 | let connections = []; 125 | 126 | function isFinished(activity) { 127 | return find(processInstance.trace, matchPattern({ activityId: activity.id })).endTime !== null; 128 | } 129 | 130 | function getConnections(activity) { 131 | const incoming = filter(activity.incoming, connection => { 132 | const found = find(activities, matchPattern({ id: connection.source.id })); 133 | 134 | const finished = isFinished(found); 135 | 136 | return found && finished; 137 | }); 138 | 139 | const outgoing = filter(activity.outgoing, connection => { 140 | const found = find(activities, matchPattern({ id: connection.target.id })); 141 | 142 | const finished = isFinished(activity); 143 | 144 | return found && finished; 145 | }); 146 | 147 | return [ 148 | ...incoming, 149 | ...outgoing 150 | ]; 151 | } 152 | 153 | forEach(activities, activity => { 154 | connections = uniqueBy('id', [ 155 | ...connections, 156 | ...getConnections(activity) 157 | ]); 158 | }); 159 | 160 | return connections; 161 | } 162 | 163 | _getDottedConnections(connections) { 164 | let dottedConnections = []; 165 | 166 | connections.forEach(connection => { 167 | const { target } = connection; 168 | 169 | connections.forEach(c => { 170 | const { source } = c; 171 | 172 | if (source === target) { 173 | dottedConnections.push({ 174 | waypoints: [ 175 | connection.waypoints[ connection.waypoints.length - 1], 176 | getMid(target), 177 | c.waypoints[0] 178 | ] 179 | }); 180 | } 181 | }); 182 | }); 183 | 184 | return dottedConnections; 185 | } 186 | 187 | _addActivityButton(activity, activityId, processInstanceId) { 188 | if (is(activity, 'bpmn:UserTask')) { 189 | const url = getTasklistUrl(activityId, processInstanceId); 190 | 191 | this._addOverlay({ 192 | element: activity, 193 | html: domify(` 194 | 195 | 196 | Tasklist 197 | 198 | `) 199 | }); 200 | } 201 | 202 | if (isExternalTask(activity)) { 203 | 204 | const topic = getExternalTaskTopic(activity); 205 | 206 | this._addOverlay({ 207 | element: activity, 208 | html: domify(` 209 | 210 | 211 | 212 | Execute through ${topic} topic using an external task worker. 213 | 214 | 215 | `) 216 | }); 217 | } 218 | } 219 | 220 | _addOverlay({ element, html, position }) { 221 | var defaultPosition = { 222 | top: OFFSET_TOP, 223 | right: OFFSET_RIGHT 224 | }; 225 | 226 | this._overlays.add(element, 'process-instance', { 227 | position: position || defaultPosition, 228 | html: html, 229 | show: { 230 | minZoom: 0.5 231 | } 232 | }); 233 | } 234 | 235 | _removeOverlays() { 236 | this._overlays.remove({ type: 'process-instance' }); 237 | } 238 | 239 | _addConnectionMarker(connection, attrs = {}) { 240 | 241 | attrs = assign({ 242 | stroke: FILL, 243 | strokeWidth: 4 244 | }, attrs); 245 | 246 | svgAppend(this._getLayer(), createCurve(connection.waypoints, attrs)); 247 | } 248 | 249 | _getLayer() { 250 | return this._canvas.getLayer('processInstance', 1); 251 | } 252 | 253 | clear() { 254 | svgClear(this._getLayer()); 255 | 256 | this._removeOverlays(); 257 | } 258 | } 259 | 260 | ProcessInstance.$inject = [ 261 | 'canvas', 262 | 'elementRegistry', 263 | 'eventBus', 264 | 'overlays' 265 | ]; 266 | 267 | function getTasklistUrl(taskId, processInstanceId) { 268 | const searchQuery = [ 269 | { 270 | type: 'processInstanceId', 271 | operator: 'eq', 272 | value: processInstanceId, 273 | name: '' 274 | } 275 | ]; 276 | 277 | const url = `http://localhost:8080/camunda/app/tasklist/default/#/?searchQuery=${ JSON.stringify(searchQuery) }&task=${ taskId }`; 278 | 279 | return encodeURI(url); 280 | } 281 | 282 | function getMid(shape) { 283 | return { 284 | x: shape.x + shape.width / 2, 285 | y: shape.y + shape.height / 2 286 | }; 287 | } 288 | 289 | function isExternalTask(activity) { 290 | const like = is(activity, 'camunda:ServiceTaskLike'); 291 | 292 | if (!like) { 293 | return false; 294 | } 295 | 296 | return !!getExternalTaskTopic(activity); 297 | } 298 | -------------------------------------------------------------------------------- /packages/app/lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const polka = require('polka'); 4 | 5 | const sirv = require('sirv'); 6 | 7 | const opn = require('open'); 8 | 9 | const getPort = require('get-port'); 10 | 11 | const jsonBody = require('body/json'); 12 | 13 | const { 14 | readFile, 15 | hash 16 | } = require('./util'); 17 | 18 | const middlewares = [ failSafe, compat ]; 19 | 20 | const EngineApi = require('./engine-api'); 21 | 22 | 23 | function json() { 24 | 25 | return (req, res, next) => jsonBody(req, res, (err, result) => { 26 | 27 | if (err) { 28 | return res.status(400).json({}); 29 | } else { 30 | req.body = result; 31 | 32 | next(); 33 | } 34 | }); 35 | } 36 | 37 | const staticDirectory = path.resolve(__dirname + '/../static'); 38 | 39 | async function create(options) { 40 | 41 | const diagramPath = options.diagramPath; 42 | const verbose = options.verbose; 43 | 44 | const engine = new EngineApi(options.camundaBase || 'http://localhost:8080'); 45 | 46 | const app = polka(); 47 | 48 | let uploadedDiagram; 49 | 50 | let diagram; 51 | 52 | let runError; 53 | 54 | let deployment; 55 | 56 | let processDefinition; 57 | 58 | let processInstance; 59 | 60 | let fetchedDetails; 61 | 62 | function getDiagram() { 63 | return uploadedDiagram || getLocalDiagram(); 64 | } 65 | 66 | function getLocalDiagram() { 67 | 68 | if (!diagramPath) { 69 | return null; 70 | } 71 | 72 | const diagram = readFile(diagramPath); 73 | 74 | const { 75 | contents 76 | } = diagram; 77 | 78 | return { 79 | ...diagram, 80 | hash: hash(contents) 81 | }; 82 | } 83 | 84 | async function getInstanceDetails() { 85 | 86 | if (!processInstance) { 87 | return null; 88 | } 89 | 90 | const { 91 | id, 92 | definitionId 93 | } = processInstance; 94 | 95 | const { 96 | diagramHash 97 | } = deployment; 98 | 99 | const details = await engine.getProcessInstanceDetails(processInstance); 100 | 101 | return { 102 | id, 103 | definitionId, 104 | diagramHash, 105 | state: getStateFromDetails(details), 106 | trace: getTraceFromDetails(details) 107 | }; 108 | } 109 | 110 | function clear() { 111 | deployment = null; 112 | processDefinition = null; 113 | processInstance = null; 114 | runError = null; 115 | } 116 | 117 | async function reload() { 118 | 119 | try { 120 | diagram = await getDiagram(); 121 | 122 | if (!diagram) { 123 | console.debug('No diagram to load'); 124 | 125 | return clear(); 126 | } 127 | 128 | const processInstance = await deployAndRun(diagram); 129 | 130 | if (processInstance) { 131 | console.log('Process deployed and instance started'); 132 | } 133 | } catch (err) { 134 | console.warn('Failed to run diagram', err); 135 | } 136 | } 137 | 138 | async function deployAndRun(newDiagram) { 139 | 140 | clear(); 141 | 142 | try { 143 | deployment = { 144 | ...await engine.deployDiagram(newDiagram), 145 | diagramHash: hash(newDiagram.contents) 146 | }; 147 | 148 | processDefinition = deployment.deployedProcessDefinition; 149 | 150 | if (!processDefinition) { 151 | runError = { 152 | message: 'No executable process', 153 | details: 'No process in the diagram marked as isExecutable', 154 | state: 'NOT_RUNNABLE' 155 | }; 156 | 157 | return; 158 | } 159 | 160 | processInstance = await engine.startProcessInstance(processDefinition); 161 | 162 | fetchedDetails = getInstanceDetails(); 163 | 164 | return processInstance; 165 | } catch (err) { 166 | runError = { 167 | message: err.message, 168 | details: err.details 169 | }; 170 | 171 | throw err; 172 | } 173 | } 174 | 175 | function getStateFromDetails(details) { 176 | const { 177 | endTime, 178 | canceled 179 | } = details.state; 180 | 181 | if (canceled) { 182 | return 'canceled'; 183 | } 184 | 185 | if (!endTime) { 186 | return 'running'; 187 | } 188 | 189 | return 'completed'; 190 | } 191 | 192 | function getTraceFromDetails(details) { 193 | 194 | const { activityInstances } = details; 195 | 196 | return activityInstances.reduce((instancesById, instance) => { 197 | 198 | const { 199 | id, 200 | parentActivityInstanceId, 201 | activityId, 202 | activityType, 203 | startTime, 204 | endTime, 205 | durationInMillis, 206 | taskId 207 | } = instance; 208 | 209 | instancesById[id] = { 210 | id, 211 | parentActivityInstanceId, 212 | activityId, 213 | activityType, 214 | startTime, 215 | endTime, 216 | durationInMillis, 217 | taskId 218 | }; 219 | 220 | return instancesById; 221 | }, {}); 222 | } 223 | 224 | // api ////////////////////// 225 | 226 | app.put('/api/deploy', ...middlewares, async (req, res, next) => { 227 | 228 | const diagram = getDiagram(); 229 | 230 | if (!diagram) { 231 | return res.status(412).json({ 232 | message: 'no diagram' 233 | }); 234 | } 235 | 236 | try { 237 | deployment = await engine.deployDiagram(diagram); 238 | 239 | return res.json(deployment); 240 | } catch (err) { 241 | console.error('failed to deploy diagram', err); 242 | 243 | return res.status(500).json({ 244 | message: 'diagram could not be deployed' 245 | }); 246 | } 247 | }); 248 | 249 | app.post('/api/process-instance/start', ...middlewares, async (req, res, next) => { 250 | 251 | if (!processDefinition) { 252 | return res.status(412).json({ 253 | message: 'no deployed process definition' 254 | }); 255 | } 256 | 257 | try { 258 | processInstance = await engine.startProcessInstance(processDefinition); 259 | 260 | console.log('process instance restarted'); 261 | 262 | return res.json(processInstance); 263 | } catch (err) { 264 | console.error('failed to deploy diagram', err); 265 | 266 | return res.status(500).json({ 267 | message: 'failed to start diagram' 268 | }); 269 | } 270 | }); 271 | 272 | app.get('/api/diagram', ...middlewares, async (req, res, next) => { 273 | 274 | const diagram = await getDiagram(); 275 | 276 | if (!diagram) { 277 | return res.status(404).json({ 278 | message: 'no diagram' 279 | }); 280 | } 281 | 282 | return res.json(diagram); 283 | }); 284 | 285 | app.put('/api/diagram', ...middlewares, json(), async (req, res, next) => { 286 | 287 | const { 288 | contents, 289 | name 290 | } = req.body; 291 | 292 | uploadedDiagram = { 293 | contents, 294 | name, 295 | path: null, 296 | hash: hash(contents), 297 | mtimeMs: -1 298 | }; 299 | 300 | // deploy asynchronously 301 | deployAndRun(uploadedDiagram).catch(err => { 302 | console.error('failed to deploy uploaded diagram', err); 303 | }); 304 | 305 | return res.status(201).json(uploadedDiagram); 306 | }); 307 | 308 | app.get('/api/process-instance', ...middlewares, async (req, res, next) => { 309 | 310 | if (runError) { 311 | return res.status(400).json(runError); 312 | } 313 | 314 | const details = await fetchedDetails; 315 | 316 | return res.json(details); 317 | }); 318 | 319 | app.post('/api/diagram/open-external', ...middlewares, async (req, res, next) => { 320 | 321 | const diagram = getDiagram(); 322 | 323 | if (!diagram) { 324 | return res.status(404).json({ 325 | message: 'no diagram' 326 | }); 327 | } 328 | 329 | if (!diagram.path) { 330 | return res.status(412).json({ 331 | message: 'diagram has no path' 332 | }); 333 | } 334 | 335 | const app = options.diagramEditor; 336 | 337 | console.log('Opening diagram in %s...', app || 'external tool'); 338 | 339 | await opn(diagram.path, app ? { app: { name: app } } : {}); 340 | 341 | return res.json({}); 342 | }); 343 | 344 | 345 | // static resources 346 | 347 | app.use('/', sirv(staticDirectory, { dev: process.env.NODE_ENV === 'development' })); 348 | 349 | app.get('/', (req, res, next) => { 350 | res.sendFile(path.join(staticDirectory, 'index.html')); 351 | }); 352 | 353 | const port = await getPort({ port: options.port }); 354 | 355 | setInterval(async function() { 356 | 357 | const t = Date.now(); 358 | 359 | try { 360 | fetchedDetails = getInstanceDetails(); 361 | 362 | await fetchedDetails; 363 | verbose && console.debug('Fetched instance details (t=%sms)', Date.now() - t); 364 | } catch (err) { 365 | console.error('Failed to fetch instance details', err); 366 | } 367 | }, 2000); 368 | 369 | setTimeout(reload, 0); 370 | 371 | setInterval(async function() { 372 | 373 | try { 374 | let newDiagram = await getDiagram(); 375 | 376 | let tsOld = diagram ? diagram.mtimeMs : -1; 377 | let tsNew = newDiagram ? newDiagram.mtimeMs : -1; 378 | 379 | if (tsOld < tsNew) { 380 | 381 | // diagram changed externally, reloading 382 | console.debug('Diagram changed externally, reloading'); 383 | 384 | reload(); 385 | } 386 | } catch (err) { 387 | console.error('External change check failed', err); 388 | } 389 | }, 500); 390 | 391 | 392 | return new Promise((resolve, reject) => { 393 | app.listen(port, (err) => { 394 | if (err) { 395 | return reject(err); 396 | } 397 | 398 | return resolve(`http://localhost:${port}`); 399 | }); 400 | 401 | }); 402 | 403 | } 404 | 405 | module.exports.create = create; 406 | 407 | // helpers /////////////// 408 | 409 | async function failSafe(req, res, next) { 410 | 411 | try { 412 | await next(); 413 | } catch (err) { 414 | console.error('unhandled route error', err); 415 | 416 | res.status(500).send(); 417 | } 418 | } 419 | 420 | function compat(req, res, next) { 421 | 422 | res.status = function(status) { 423 | this.statusCode = status; 424 | 425 | return this; 426 | }; 427 | 428 | res.json = function(obj) { 429 | const json = JSON.stringify(obj); 430 | 431 | this.setHeader('Content-Type', 'application/json'); 432 | 433 | return this.end(json); 434 | }; 435 | 436 | next(); 437 | } -------------------------------------------------------------------------------- /packages/client/src/Client.svelte: -------------------------------------------------------------------------------- 1 | 124 | 125 | 243 | 244 | 245 | 246 | 247 | diagramLoading = false } 251 | onLoading={ () => diagramLoading = true } 252 | /> 253 | 254 | 255 | {#if runError} 256 | 257 |
258 |

Failed to run: {runError.message}

259 | 260 | {#if runError.details} 261 |
{JSON.stringify(runError.details, null, '  ') }
262 | {/if} 263 |
264 | 265 | {/if} 266 | 267 | {#if diagram} 268 | 269 | {#if diagram.path} 270 | 271 | { diagram.path } 272 | 273 | {:else} 274 |
Uploaded: { diagram.name }
275 | {/if} 276 | 277 | {/if} 278 | 279 | {#if diagram} 280 | 281 |
282 | 283 | {#if runError} 284 | 285 | {:else if !instanceDetails} 286 | 287 | {:else if instanceDetails.state === 'running'} 288 | 289 | {:else} 290 | 291 | {/if} 292 | 293 |

294 | 295 | 296 | {#if runError} 297 | Failed to run 298 | {:else if !instanceDetails} 299 | Loading instance details 300 | {:else if instanceDetails.state === 'running'} 301 | Instance running 302 | {:else} 303 | Instance finished 304 | {/if} 305 | 306 | 307 | {#if instanceDetails && instanceDetails.state === 'running'} 308 | 309 | Cockpit 310 | 311 | {/if} 312 |

313 | 314 | {#if runError} 315 |
{ runError.message}
316 | {/if} 317 | 318 | {#if waitStates.length} 319 |
320 |

Waiting for...

321 | 322 | 339 |
340 | {/if} 341 | 342 | {#if instanceDetails || runError} 343 | 348 | {/if} 349 | 350 |
351 | {/if} 352 | 353 | {#await loadingDiagram} 354 | {:then diagram} 355 | {:catch error} 356 |
357 | 358 |

No Diagram Opened

359 | 360 | Drop a BPMN diagram here or pass your diagram via the command line: 361 | 362 |
camunda-playground my-diagram.bpmn
363 |
364 | {/await} --------------------------------------------------------------------------------