├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── screenshot.png ├── src ├── StepFunctionVisualization.svelte ├── app.html ├── global.d.ts ├── history.ts ├── routes │ ├── [arn].svelte │ ├── [arn] │ │ └── [execution].svelte │ ├── get │ │ ├── [arn].ts │ │ └── [arn] │ │ │ └── [execution].ts │ ├── index.svelte │ └── list.ts ├── statemachine.ts └── visualize.ts ├── static └── favicon.png ├── svelte.config.js └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /.svelte-kit 4 | /package 5 | .* 6 | !.env.example 7 | !.gitignore 8 | !.eslintrc.cjs 9 | !.npmrc 10 | !.prettierrc 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stepfunction-visualizer 2 | 3 | A toolkit to debug and visualize [local AWS step functions](https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local.html) 4 | 5 | ![a flow chart showing a step function with one state's data displayed in a debug panel to the side](screenshot.png) 6 | 7 | To use, first you need to set up local step functions. If you are using the serverless framework, I recommend using the [serverless-step-functions-local package](https://www.npmjs.com/package/serverless-step-functions-local) and following the advice in this [blog post](https://medium.com/atheneum-partners-digitalization/how-to-run-serverless-step-functions-offline-26b7b994d2b5). 8 | 9 | Once you have local step functions running at localhost:8083 (the default port), run the steps below to launch a server that shows all the local step functions and their executions, with flowcharts. 10 | 11 | ## Running the server 12 | 13 | Install dependencies with `npm install` and then start the development server: 14 | 15 | ```sh 16 | npm run dev 17 | ``` 18 | 19 | If all succeeds, you will be able to see the web application locally at http://localhost:3000. 20 | 21 | ### Technology used 22 | 23 | - [TypeScript](https://www.typescriptlang.org/): to model Amazon States Language (ASL) and data structures used in the CLI 24 | - [SvelteKit](https://kit.svelte.dev/): the web server (with Svelte for components) 25 | - [Mermaid](https://github.com/mermaid-js/mermaid): for flowcharts/diagramming 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stepfunction-visualizer", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "svelte-kit dev", 6 | "build": "svelte-kit build", 7 | "package": "svelte-kit package", 8 | "preview": "svelte-kit preview", 9 | "check": "svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", 12 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "next", 16 | "@sveltejs/kit": "next", 17 | "@typescript-eslint/eslint-plugin": "^4.31.1", 18 | "@typescript-eslint/parser": "^4.31.1", 19 | "eslint": "^7.32.0", 20 | "eslint-config-prettier": "^8.3.0", 21 | "eslint-plugin-svelte3": "^3.2.1", 22 | "prettier": "^2.4.1", 23 | "prettier-plugin-svelte": "^2.4.0", 24 | "svelte": "^3.44.0", 25 | "svelte-check": "^2.2.6", 26 | "svelte-preprocess": "^4.9.4", 27 | "tslib": "^2.3.1", 28 | "typescript": "^4.4.3" 29 | }, 30 | "type": "module", 31 | "dependencies": { 32 | "@aws-sdk/client-sfn": "^3.47.0", 33 | "@types/mermaid": "^8.2.7", 34 | "mermaid": "^8.13.9" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freedmand/stepfunction-visualizer/fc2370b2888c48a3f9bb72aa17e1f134ce904b7d/screenshot.png -------------------------------------------------------------------------------- /src/StepFunctionVisualization.svelte: -------------------------------------------------------------------------------- 1 | 124 | 125 |

Definition

126 | 127 | 128 | 131 | 156 | 157 |
129 |
130 |
132 | {#if selectedInput != null} 133 |
134 | Input 135 |
{JSON.stringify(selectedInput, null, JSON_INDENT)}
136 |
137 | {/if} 138 | {#if selectedOutput != null} 139 |
140 | Output 141 |
{JSON.stringify(selectedOutput, null, JSON_INDENT)}
142 |
143 | {/if} 144 |
145 | Details 146 |
{JSON.stringify(
147 | 						selectedState,
148 | 						(k, v) => {
149 | 							if (k == 'States') return Object.keys(v);
150 | 							return v;
151 | 						},
152 | 						JSON_INDENT
153 | 					)}
154 |
155 |
158 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 30 | %svelte.head% 31 | 32 | 33 |
%svelte.body%
34 | 35 | 36 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/history.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionEvent, ExecutionHistory, StateEntered, StateExited } from './statemachine'; 2 | 3 | export type Status = 'success' | 'fail' | 'neverrun' | 'indeterminate'; 4 | 5 | type EventType = 'task' | 'parallel' | 'map' | 'choice' | 'pass' | 'fail' | 'succeed' | 'wait' | null; 6 | 7 | export interface Details { 8 | status: Status; 9 | input: JSON; 10 | output: JSON; 11 | } 12 | 13 | /** 14 | * A class that wraps an execution history of a step function 15 | * and provides utility functions to get the status of each 16 | * step 17 | */ 18 | export class History { 19 | constructor(readonly history: ExecutionHistory) { } 20 | 21 | getById(id: string): ExecutionEvent | null { 22 | // Find an event by id 23 | for (const event of this.history.events) { 24 | if (event.id == id) return event; 25 | } 26 | return null; 27 | } 28 | 29 | getByPreviousId(id: string): ExecutionEvent | null { 30 | // Find an event that matches the specified previous id 31 | for (const event of this.history.events) { 32 | if (event.previousEventId == id) return event; 33 | } 34 | return null; 35 | } 36 | 37 | getInputEvent(event: ExecutionEvent | null, type: EventType): StateEntered | null { 38 | // Get the input event corresponding to a given event 39 | if (event == null) return null; 40 | if ( 41 | ((type == 'task' || type == null) && event.type == 'TaskStateEntered') || 42 | ((type == 'parallel' || type == null) && event.type == 'ParallelStateEntered') || 43 | ((type == 'map' || type == null) && event.type == 'MapStateEntered') || 44 | ((type == 'choice' || type == null) && event.type == 'ChoiceStateEntered') || 45 | ((type == 'pass' || type == null) && event.type == 'PassStateEntered') || 46 | ((type == 'fail' || type == null) && event.type == 'FailStateEntered') || 47 | ((type == 'succeed' || type == null) && event.type == 'SucceedStateEntered') || 48 | ((type == 'wait' || type == null) && event.type == 'WaitStateEntered') 49 | ) { 50 | return event; 51 | } 52 | return this.getInputEvent(this.getById(event.previousEventId), type); 53 | } 54 | 55 | getOutputEvent(event: ExecutionEvent | null, type: EventType): StateExited | null { 56 | // Get the output event corresponding to a given event 57 | if (event == null) return null; 58 | if ( 59 | ((type == 'task' || type == null) && event.type == 'TaskStateExited') || 60 | ((type == 'parallel' || type == null) && event.type == 'ParallelStateExited') || 61 | ((type == 'map' || type == null) && event.type == 'MapStateExited') || 62 | ((type == 'choice' || type == null) && event.type == 'ChoiceStateExited') || 63 | ((type == 'pass' || type == null) && event.type == 'PassStateExited') || 64 | ((type == 'fail' || type == null) && event.type == 'FailStateExited') || 65 | ((type == 'succeed' || type == null) && event.type == 'SucceedStateExited') || 66 | ((type == 'wait' || type == null) && event.type == 'WaitStateExited') 67 | ) { 68 | return event; 69 | } 70 | return this.getOutputEvent(this.getByPreviousId(event.id), type); 71 | } 72 | 73 | getType(event: ExecutionEvent): EventType { 74 | // Get the type of an execution event 75 | // TODO: support Map and other types 76 | if (event.type.startsWith('Parallel')) return 'parallel'; 77 | if (event.type.startsWith('Task')) return 'task'; 78 | if (event.type.startsWith('Map')) return 'map'; 79 | if (event.type.startsWith('Choice')) return 'choice'; 80 | if (event.type.startsWith('Pass')) return 'pass'; 81 | if (event.type.startsWith('Fail')) return 'fail'; 82 | if (event.type.startsWith('Succeed')) return 'succeed'; 83 | if (event.type.startsWith('Wait')) return 'wait'; 84 | return null; 85 | } 86 | 87 | getStatus(name: string): Status { 88 | return this.getDetails(name).status; 89 | } 90 | 91 | getDetails(name: string): Details { 92 | // Get the details of a state in the execution history by name 93 | // (details returns status and input/output if available) 94 | let status: Status = 'neverrun'; 95 | let input: JSON = null; 96 | let output: JSON = null; 97 | for (const event of this.history.events) { 98 | // Iterate through each event to track the state 99 | if (event.type == 'TaskStateEntered' || event.type == 'ParallelStateEntered' || event.type == 'MapStateEntered' || event.type == 'ChoiceStateEntered' || event.type == 'PassStateEntered') { 100 | if (event.stateEnteredEventDetails.name == name) { 101 | status = 'indeterminate'; 102 | input = JSON.parse(event.stateEnteredEventDetails.input); 103 | } 104 | } 105 | 106 | if (event.type == 'LambdaFunctionSucceeded' || event.type == 'ParallelStateSucceeded' || event.type == 'MapStateExited' || event.type == 'ChoiceStateExited' || event.type == 'PassStateExited') { 107 | const eventType = this.getType(event); 108 | const startEvent = this.getInputEvent(event, eventType); 109 | const endEvent = this.getOutputEvent(event, eventType); 110 | if (startEvent.stateEnteredEventDetails.name == name) { 111 | status = 'success'; 112 | input = JSON.parse(startEvent.stateEnteredEventDetails.input); 113 | if (endEvent != null) { 114 | output = JSON.parse(endEvent.stateExitedEventDetails.output); 115 | } 116 | } 117 | } 118 | 119 | if (event.type == 'LambdaFunctionFailed') { 120 | const eventType = this.getType(event); 121 | const startEvent = this.getInputEvent(event, eventType); 122 | const endEvent = this.getOutputEvent(event, eventType); 123 | 124 | if (startEvent.stateEnteredEventDetails.name == name) { 125 | status = 'fail'; 126 | input = JSON.parse(startEvent.stateEnteredEventDetails.input); 127 | if (endEvent != null) { 128 | output = JSON.parse(endEvent.stateExitedEventDetails.output); 129 | } 130 | return { input, output, status }; 131 | } 132 | } 133 | } 134 | 135 | return { input, output, status }; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/routes/[arn].svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 35 | 36 |

Step Functions / {description.name}

37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 55 | 58 | 61 | 62 |
StatusMachine ARNRole ARNTypeCreation Date
47 | {description.status} 48 | 50 | {description.stateMachineArn} 51 | 53 | {description.roleArn} 54 | 56 | {description.type} 57 | 59 | {description.creationDate} 60 |
63 | 64 |

Executions

65 | 66 | {#if executions.length == 0} 67 |

No executions found. Run the step function to create one.

68 | {:else} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {#each executions as execution} 78 | 79 | 82 | 85 | 88 | 91 | 94 | 95 | {/each} 96 |
NameStatusExecution ARNStart DateStop Date
80 | {execution.name} 81 | 83 | {execution.status} 84 | 86 | {execution.executionArn} 87 | 89 | {execution.startDate} 90 | 92 | {execution.stopDate} 93 |
97 | {/if} 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/routes/[arn]/[execution].svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | 40 |

Step Functions / {description.name} / {execution.name}

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 58 | 61 | 64 | 67 | 70 | 71 |
NameStatusExecution ARNInputStart DateStop Date
53 | {execution.name} 54 | 56 | {execution.status} 57 | 59 | {execution.executionArn} 60 | 62 | {execution.input} 63 | 65 | {execution.startDate} 66 | 68 | {execution.stopDate} 69 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /src/routes/get/[arn].ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit/types/endpoint'; 2 | import { SFN } from '@aws-sdk/client-sfn'; 3 | 4 | const sfn = new SFN({ 5 | endpoint: process.env.AWS_ENDPOINT ?? 'http://localhost:8083' 6 | }); 7 | 8 | export const get: RequestHandler = async ({ params }) => { 9 | const arn = params.arn; 10 | const definition = await sfn.describeStateMachine({ stateMachineArn: arn }); 11 | const executions = await sfn.listExecutions({ stateMachineArn: arn }); 12 | return { 13 | body: { 14 | definition, 15 | executions 16 | } 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/routes/get/[arn]/[execution].ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit/types/endpoint'; 2 | import { SFN } from '@aws-sdk/client-sfn'; 3 | 4 | const sfn = new SFN({ 5 | endpoint: process.env.AWS_ENDPOINT ?? 'http://localhost:8083' 6 | }); 7 | 8 | export const get: RequestHandler = async ({ params }) => { 9 | const arn = params.arn; 10 | const executionArn = params.execution; 11 | const definition = await sfn.describeStateMachine({ stateMachineArn: arn }); 12 | const execution = await sfn.describeExecution({ executionArn }); 13 | const history = await sfn.getExecutionHistory({ 14 | executionArn 15 | }); 16 | return { 17 | body: { 18 | definition, 19 | execution, 20 | history 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 19 | 20 |

Step Functions

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {#each stateMachines as stateMachine} 29 | 30 | 33 | 36 | 39 | 42 | 43 | {/each} 44 |
NameARNTypeCreation Date
31 | {stateMachine.name} 32 | 34 | {stateMachine.stateMachineArn} 35 | 37 | {stateMachine.type} 38 | 40 | {stateMachine.creationDate} 41 |
45 | -------------------------------------------------------------------------------- /src/routes/list.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit/types/endpoint'; 2 | import { execSync } from 'child_process'; 3 | 4 | export const get: RequestHandler = async () => { 5 | const output = execSync( 6 | 'aws stepfunctions --endpoint http://localhost:8083 --output json list-state-machines' 7 | ); 8 | return { 9 | body: output 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/statemachine.ts: -------------------------------------------------------------------------------- 1 | export interface StateMachine { 2 | stateMachineArn: string; 3 | name: string; 4 | type: string; 5 | creationDate: string; 6 | } 7 | 8 | export interface ListResponse { 9 | stateMachines: StateMachine[]; 10 | } 11 | 12 | export interface GetResponse { 13 | definition: StateMachineDescription; 14 | executions: { 15 | executions: Execution[]; 16 | }; 17 | } 18 | 19 | export interface ExecutionGetResponse { 20 | definition: StateMachineDescription; 21 | execution: ExecutionDefinition; 22 | history: ExecutionHistory; 23 | } 24 | 25 | export interface Execution { 26 | name: string; 27 | status: string; 28 | executionArn: string; 29 | stateMachineArn: string; 30 | startDate: string; 31 | stopDate: string; 32 | } 33 | 34 | export interface ExecutionDefinition extends Execution { 35 | input: string; 36 | } 37 | 38 | export interface StateMachineDescription extends StateMachine { 39 | status: string; 40 | roleArn: string; 41 | definition: string; 42 | } 43 | 44 | export interface StepFunction { 45 | StartAt: string; 46 | States: { [state: string]: State }; 47 | Comment?: string; 48 | TimeoutSeconds?: number; 49 | Version?: string; 50 | } 51 | 52 | interface BaseState { 53 | Type: string; 54 | Comment?: string; 55 | End?: boolean; 56 | Next?: string; 57 | Catch?: Catcher[]; 58 | } 59 | 60 | export type State = PassState | TaskState | ParallelState | MapState | ChoiceState | FailState | SucceedState | WaitState; 61 | 62 | export interface PassState extends BaseState { 63 | Type: 'Pass'; 64 | ResultPath?: string; 65 | Result?: JSON; 66 | Parameters?: JSON; 67 | } 68 | 69 | export interface TaskState extends BaseState { 70 | Type: 'Task'; 71 | Resource: string; 72 | ResultPath?: string; 73 | ResultSelector?: string; 74 | Parameters?: JSON; 75 | Retry?: JSON; 76 | TimeoutSeconds?: number; 77 | } 78 | 79 | export interface ParallelState extends BaseState { 80 | Type: 'Parallel'; 81 | Branches: StepFunction[]; 82 | ResultPath?: string; 83 | ResultSelector?: string; 84 | Retry?: JSON; 85 | } 86 | 87 | export interface MapState extends BaseState { 88 | Type: 'Map'; 89 | Iterator: StepFunction; 90 | ItemsPath?: string; 91 | MaxConcurrency?: number; 92 | ResultPath?: string; 93 | ResultSelector?: string; 94 | Retry?: JSON; 95 | } 96 | 97 | export interface ChoiceState extends BaseState { 98 | Type: 'Choice'; 99 | Choices: { 100 | Next: string; 101 | }[]; 102 | Default?: string; 103 | } 104 | 105 | export interface FailState extends BaseState { 106 | Type: 'Fail'; 107 | Cause?: string; 108 | Error?: string; 109 | } 110 | 111 | export interface SucceedState extends BaseState { 112 | Type: 'Succeed'; 113 | } 114 | 115 | export interface WaitState extends BaseState { 116 | Type: 'Wait'; 117 | Seconds?: number; 118 | Timestamp?: string; 119 | SecondsPath?: string; 120 | TimestampPath?: string; 121 | } 122 | 123 | export interface Catcher { 124 | ErrorEquals: string[]; 125 | Next: string; 126 | ResultPath?: string; 127 | } 128 | 129 | export interface ExecutionHistory { 130 | events: ExecutionEvent[]; 131 | } 132 | 133 | export interface BaseExecutionEvent { 134 | timestamp: string; 135 | id: string; 136 | previousEventId: string; 137 | } 138 | 139 | export type ExecutionEvent = 140 | | ExecutionStarted 141 | | StateEntered 142 | | LambdaFunctionScheduled 143 | | LambdaFunctionStarted 144 | | LambdaFunctionSucceeded 145 | | StateExited 146 | | ParallelStateSucceeded 147 | | LambdaFunctionFailed 148 | | ExecutionFailed; 149 | 150 | export interface ExecutionStarted extends BaseExecutionEvent { 151 | type: 'ExecutionStarted'; 152 | executionStartedEventDetails: { 153 | input: string; 154 | inputDetails: InputDetails; 155 | roleArn: string; 156 | }; 157 | } 158 | 159 | export interface StateEntered extends BaseExecutionEvent { 160 | type: 'TaskStateEntered' | 'ParallelStateEntered' | 'MapStateEntered' | 'ChoiceStateEntered' | 'PassStateEntered' | 'FailStateEntered' | 'SucceedStateEntered' | 'WaitStateEntered'; 161 | stateEnteredEventDetails: { 162 | name: string; 163 | input: string; 164 | inputDetails: InputDetails; 165 | }; 166 | } 167 | 168 | export interface LambdaFunctionScheduled extends BaseExecutionEvent { 169 | type: 'LambdaFunctionScheduled'; 170 | lambdaFunctionScheduledEventDetails: { 171 | resource: string; 172 | input: string; 173 | inputDetails: InputDetails; 174 | }; 175 | } 176 | 177 | export interface LambdaFunctionStarted extends BaseExecutionEvent { 178 | type: 'LambdaFunctionStarted'; 179 | } 180 | 181 | export interface LambdaFunctionSucceeded extends BaseExecutionEvent { 182 | type: 'LambdaFunctionSucceeded'; 183 | lambdaFunctionSucceededEventDetails: { 184 | output: string; 185 | outputDetails: OutputDetails; 186 | }; 187 | } 188 | 189 | export interface StateExited extends BaseExecutionEvent { 190 | type: 'TaskStateExited' | 'ParallelStateExited' | 'MapStateExited' | 'ChoiceStateExited' | 'PassStateExited' | 'FailStateExited' | 'SucceedStateExited' | 'WaitStateExited'; 191 | stateExitedEventDetails: { 192 | name: string; 193 | output: string; 194 | outputDetails: OutputDetails; 195 | }; 196 | } 197 | 198 | export interface ParallelStateSucceeded extends BaseExecutionEvent { 199 | type: 'ParallelStateSucceeded'; 200 | } 201 | 202 | export interface LambdaFunctionFailed extends BaseExecutionEvent { 203 | type: 'LambdaFunctionFailed'; 204 | lambdaFunctionFailedEventDetails: { 205 | error: string; 206 | cause: string; 207 | }; 208 | } 209 | 210 | export interface ExecutionFailed extends BaseExecutionEvent { 211 | type: 'ExecutionFailed'; 212 | executionFailedEventDetails: { 213 | error: string; 214 | cause: string; 215 | }; 216 | } 217 | 218 | export interface InputDetails { 219 | truncated: boolean; 220 | } 221 | 222 | export interface OutputDetails { 223 | truncated: boolean; 224 | } 225 | -------------------------------------------------------------------------------- /src/visualize.ts: -------------------------------------------------------------------------------- 1 | import type { History } from './history'; 2 | import type { ChoiceState, State, StepFunction, TaskState } from './statemachine'; 3 | 4 | const STATE_FILLS = { 5 | success: 'lightgreen', 6 | fail: 'lightcoral', 7 | neverrun: 'gainsboro', 8 | indeterminate: 'grey' 9 | }; 10 | 11 | const DEFAULT_BRACE = ['[', ']']; 12 | const BRACES = { 13 | Choice: ['([', '])'], 14 | Pass: ['((', '))'], 15 | Fail: ['[/', '\\]'], 16 | Succeed: ['[\\', '/]'], 17 | Wait: ['[/', '/]'] 18 | }; 19 | 20 | class Context { 21 | public id = 0; 22 | public states: { [id: string]: State } = {}; 23 | 24 | constructor(readonly history: History | null = null) { } 25 | 26 | getId(): string { 27 | return `s${this.id++}`; 28 | } 29 | 30 | registerState(id: string, state: State) { 31 | this.states[id] = state; 32 | } 33 | } 34 | 35 | const DIRECTION = 'TB'; 36 | 37 | export function stepFunctionToMermaid( 38 | stepFunction: StepFunction, 39 | history: History | null 40 | ): { 41 | flowchart: string; 42 | context: Context; 43 | } { 44 | const context = new Context(history); 45 | return { 46 | flowchart: `flowchart ${DIRECTION} 47 | ${convertStepFunction(context, stepFunction)}`, 48 | context 49 | }; 50 | } 51 | 52 | function convertStepFunction( 53 | context: Context, 54 | stepFunction: StepFunction 55 | ): string { 56 | const id = context.getId(); 57 | 58 | const getId = (stateKey) => `${id}-${stateKey}_`; 59 | 60 | let result = ''; 61 | result += `${getId(stepFunction.StartAt)}\n`; 62 | for (const stateKey of Object.keys(stepFunction.States)) { 63 | const state = stepFunction.States[stateKey]; 64 | context.registerState(getId(stateKey), state); 65 | result += convertNode(context, getId, stateKey, state); 66 | const brace = BRACES[state.Type] || DEFAULT_BRACE; 67 | result += `${getId(stateKey)}${brace[0]}${JSON.stringify(stateKey)}${brace[1]}\n`; 68 | if (context.history != null) { 69 | const nodeState = context.history.getStatus(stateKey); 70 | result += `style ${getId(stateKey)} fill:${STATE_FILLS[nodeState]}\n`; 71 | } 72 | if (state.Next != null) { 73 | result += `${getId(stateKey)} --> ${getId(state.Next)}\n`; 74 | } 75 | if (state.Catch != null) { 76 | // Handle catcher next states 77 | for (const catcher of state.Catch) { 78 | result += `${getId(stateKey)} -.-> ${getId(catcher.Next)}\n`; 79 | } 80 | } 81 | } 82 | return result; 83 | } 84 | 85 | export function convertNode( 86 | context: Context, 87 | getId: (string) => string, 88 | stateKey: string, 89 | state: State 90 | ): string { 91 | switch (state.Type) { 92 | case 'Pass': 93 | case 'Task': 94 | case 'Fail': 95 | case 'Succeed': 96 | case 'Wait': 97 | return `${getId(stateKey)}[${JSON.stringify(stateKey)}]\n`; 98 | case 'Parallel': 99 | return `subgraph ${getId(stateKey)} [${JSON.stringify(stateKey)}] 100 | direction ${DIRECTION} 101 | ${state.Branches.map((branch) => convertStepFunction(context, branch)).join('\n')} 102 | end\n`; 103 | case 'Map': 104 | return `subgraph ${getId(stateKey)} [${JSON.stringify(stateKey)}] 105 | style ${getId(stateKey)} stroke-dasharray: 5 5 106 | click ${getId(stateKey)} callback "tooltip" 107 | direction ${DIRECTION} 108 | ${convertStepFunction(context, state.Iterator)} 109 | end\n`; 110 | case 'Choice': 111 | return `${convertChoice(context, getId, stateKey, state)}\n`; 112 | } 113 | } 114 | 115 | export function convertChoice( 116 | context: Context, 117 | getId: (string) => string, 118 | stateKey: string, 119 | choice: ChoiceState 120 | ): string { 121 | const possibleNextStates: string[] = []; 122 | const pushToNext = (s: string): void => { 123 | if (possibleNextStates.includes(s)) return; 124 | possibleNextStates.push(s); 125 | }; 126 | for (const { Next } of choice.Choices) { 127 | // Add all the choices 128 | pushToNext(Next); 129 | } 130 | if (choice.Default != null) { 131 | // Add the default, if set 132 | pushToNext(choice.Default); 133 | } 134 | // Create arrows for all the states 135 | let result = ''; 136 | for (const next of possibleNextStates) { 137 | result += `${getId(stateKey)} --> ${getId(next)}\n`; 138 | } 139 | return result; 140 | } 141 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freedmand/stepfunction-visualizer/fc2370b2888c48a3f9bb72aa17e1f134ce904b7d/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import preprocess from 'svelte-preprocess'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://github.com/sveltejs/svelte-preprocess 7 | // for more information about preprocessors 8 | preprocess: preprocess(), 9 | 10 | kit: { 11 | adapter: adapter(), 12 | 13 | // hydrate the
element in src/app.html 14 | target: '#svelte' 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2020", 5 | "lib": ["es2020", "DOM"], 6 | "target": "es2020", 7 | /** 8 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 9 | to enforce using \`import type\` instead of \`import\` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | To have warnings/errors of the Svelte compiler at the correct position, 16 | enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | "allowJs": true, 24 | "checkJs": true, 25 | "paths": { 26 | "$lib": ["src/lib"], 27 | "$lib/*": ["src/lib/*"] 28 | } 29 | }, 30 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] 31 | } 32 | --------------------------------------------------------------------------------