├── 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 | [](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 | 
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 | [](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 | 
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 |
313 |
314 | {#if runError}
315 |
{ runError.message}
316 | {/if}
317 |
318 | {#if waitStates.length}
319 |
320 |
Waiting for...
321 |
322 |
323 | {#each waitStates as waitState}
324 | -
325 | {#if waitState.activityType === 'parallelGateway'}
326 | Join in {waitState.activityId}
327 | {:else if waitState.activityType === 'userTask'}
328 | User task completion in {waitState.activityId}
329 | {:else if waitState.activityType === 'serviceTask'}
330 | Service task completion in {waitState.activityId}
331 | {:else if waitState.activityType === 'receiveTask'}
332 | Incoming message in {waitState.activityId}
333 | {:else}
334 | { JSON.stringify(waitState) }
335 | {/if}
336 |
337 | {/each}
338 |
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}
--------------------------------------------------------------------------------