├── .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 | 
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 |
129 |
130 | |
131 |
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 | |
156 |
157 |
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 |
37 |
38 |
39 | Status |
40 | Machine ARN |
41 | Role ARN |
42 | Type |
43 | Creation Date |
44 |
45 |
46 |
47 | {description.status}
48 | |
49 |
50 | {description.stateMachineArn}
51 | |
52 |
53 | {description.roleArn}
54 | |
55 |
56 | {description.type}
57 | |
58 |
59 | {description.creationDate}
60 | |
61 |
62 |
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 | Name |
72 | Status |
73 | Execution ARN |
74 | Start Date |
75 | Stop Date |
76 |
77 | {#each executions as execution}
78 |
79 |
80 | {execution.name}
81 | |
82 |
83 | {execution.status}
84 | |
85 |
86 | {execution.executionArn}
87 | |
88 |
89 | {execution.startDate}
90 | |
91 |
92 | {execution.stopDate}
93 | |
94 |
95 | {/each}
96 |
97 | {/if}
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/routes/[arn]/[execution].svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
39 |
40 |
41 |
42 |
43 |
44 | Name |
45 | Status |
46 | Execution ARN |
47 | Input |
48 | Start Date |
49 | Stop Date |
50 |
51 |
52 |
53 | {execution.name}
54 | |
55 |
56 | {execution.status}
57 | |
58 |
59 | {execution.executionArn}
60 | |
61 |
62 | {execution.input}
63 | |
64 |
65 | {execution.startDate}
66 | |
67 |
68 | {execution.stopDate}
69 | |
70 |
71 |
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 | Name |
24 | ARN |
25 | Type |
26 | Creation Date |
27 |
28 | {#each stateMachines as stateMachine}
29 |
30 |
31 | {stateMachine.name}
32 | |
33 |
34 | {stateMachine.stateMachineArn}
35 | |
36 |
37 | {stateMachine.type}
38 | |
39 |
40 | {stateMachine.creationDate}
41 | |
42 |
43 | {/each}
44 |
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 |
--------------------------------------------------------------------------------