├── .gitignore
├── Dockerfile
├── Dockerfile-dev
├── Dockerfile-postgres
├── LICENSE
├── README.md
├── ReadMeUtils
├── Climb.png
├── slider.gif
├── stepLatencyHeat.gif
├── steplatencychart.gif
└── timePeriodAdjust.gif
├── __test__
└── FlowChartVeiw-test.tsx
├── conf
├── defaults.ini
└── grafana.ini
├── data
└── grafana.db
├── database
├── docs
│ ├── 2024-10-30-ERD.png
│ ├── 20241011171901_create_initial_schema.sql
│ └── knex-migration-usage.md
├── migrations
│ └── 20241011171901_create_initial_schema.ts
├── seeds
│ ├── 01_step_functions.ts
│ ├── 02_steps.ts
│ ├── 03_latencies.ts
│ ├── 04_trackers.ts
│ └── utils
│ │ ├── latenciesGenerator.ts
│ │ ├── step-function-definitions.ts
│ │ └── types.ts
└── setupDatabase.ts
├── docker-compose.yml
├── eslint.config.js
├── index.html
├── knexfile.ts
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── vite.svg
├── server
├── controllers
│ ├── api
│ │ ├── averageLatenciesApiController.ts
│ │ └── stepFunctionsApiController.ts
│ ├── aws
│ │ └── getStepFunctionAWSController.ts
│ ├── client
│ │ └── clientController.ts
│ └── stepFunctionController.ts
├── docs
│ └── api.md
├── models
│ ├── averageLatenciesModel.ts
│ ├── db.ts
│ ├── dbConfig.ts
│ ├── stepAverageLatenciesModel.ts
│ ├── stepFunctionAverageLatenciesModel.ts
│ ├── stepFunctionTrackersModel.ts
│ ├── stepFunctionsModel.ts
│ ├── stepsModel.ts
│ └── types.ts
├── routes
│ ├── api
│ │ ├── averageLatenciesRouter.ts
│ │ ├── index.ts
│ │ └── stepFunctionsRouter.ts
│ └── client
│ │ └── index.ts
├── server.ts
├── types
│ ├── stepFunctionDetailsFromAWS.ts
│ ├── stepFunctionLatencyAvgsApi.ts
│ ├── stepFunctionsApi.ts
│ └── types.ts
└── utils
│ ├── listStateMachines.ts
│ └── parseStepFunction.ts
├── setupTests.js
├── src
├── App.css
├── App.tsx
├── DetailedView
│ ├── BackButton.tsx
│ ├── DataContainer.tsx
│ ├── DataVisualization.tsx
│ ├── DetailedView.tsx
│ ├── DetailedViewContainer.tsx
│ ├── DetailedViewUI.tsx
│ ├── FlowChart.tsx
│ ├── FlowChartBubble.tsx
│ ├── FlowChartDataSelector.tsx
│ ├── FlowChartView.tsx
│ ├── HeatmapChart.tsx
│ ├── StepDataVisualization.tsx
│ ├── StepFunctionSelector.tsx
│ ├── TimePeriodToggle.tsx
│ ├── TimeSlice.tsx
│ ├── TimeSlider.tsx
│ └── TimeToggle.tsx
├── assets
│ └── react.svg
├── index.css
├── landingPage
│ ├── Dashboard.tsx
│ ├── addCard.tsx
│ ├── allCards.tsx
│ ├── filterRegions.tsx
│ ├── functionCards.tsx
│ ├── landingPage.tsx
│ ├── navbar
│ │ ├── aboutPage.tsx
│ │ ├── contactUs.tsx
│ │ ├── homeButton.tsx
│ │ ├── logout.tsx
│ │ └── navBar.tsx
│ └── stepFunctionInput.tsx
├── loginForm.tsx
├── main.tsx
├── reducers
│ ├── cardSlice.tsx
│ ├── dataSlice.tsx
│ └── userSlice.tsx
├── signUpForm.tsx
└── vite-env.d.ts
├── store.tsx
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.knex.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
├── tsconfig.server.json
├── vite.config.ts
└── workers
├── StepFunction.ts
├── Tracker.ts
├── cloudWatchCronJob.ts
├── executions.ts
├── getCloudWatchData.ts
├── logs.ts
└── types.ts
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Optional stylelint cache
59 | .stylelintcache
60 |
61 | # Microbundle cache
62 | .rpt2_cache/
63 | .rts2_cache_cjs/
64 | .rts2_cache_es/
65 | .rts2_cache_umd/
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variable files
77 | .env
78 | .env.development.local
79 | .env.test.local
80 | .env.production.local
81 | .env.local
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 | .parcel-cache
86 |
87 | # Next.js build output
88 | .next
89 | out
90 |
91 | # Nuxt.js build / generate output
92 | .nuxt
93 | dist
94 |
95 | # Gatsby files
96 | .cache/
97 | # Comment in the public line in if your project uses Gatsby and not Next.js
98 | # https://nextjs.org/blog/next-9-1#public-directory-support
99 | # public
100 |
101 | # vuepress build output
102 | .vuepress/dist
103 |
104 | # vuepress v2.x temp and cache directory
105 | .temp
106 | .cache
107 |
108 | # Docusaurus cache and generated files
109 | .docusaurus
110 |
111 | # Serverless directories
112 | .serverless/
113 |
114 | # FuseBox cache
115 | .fusebox/
116 |
117 | # DynamoDB Local files
118 | .dynamodb/
119 |
120 | # TernJS port file
121 | .tern-port
122 |
123 | # Stores VSCode versions used for testing VSCode extensions
124 | .vscode-test
125 |
126 | # yarn v2
127 | .yarn/cache
128 | .yarn/unplugged
129 | .yarn/build-state.yml
130 | .yarn/install-state.gz
131 | .pnp.*
132 |
133 | #sample aws api requests / responses
134 | .aws-api
135 | .DS_Store
136 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.16
2 | WORKDIR /user/src/app
3 | COPY . /user/src/app/
4 | RUN npm install
5 | RUN npm run build
6 | RUN npm run build:server
7 | EXPOSE 3000
8 | CMD node ./dist/server/server.js
9 |
--------------------------------------------------------------------------------
/Dockerfile-dev:
--------------------------------------------------------------------------------
1 | FROM node:20.16
2 | WORKDIR /user/src/app
3 | COPY package*.json /user/src/app
4 | RUN npm install
5 | EXPOSE 3000
--------------------------------------------------------------------------------
/Dockerfile-postgres:
--------------------------------------------------------------------------------
1 | FROM postgres:12.20
2 | RUN apt-get update && apt-get upgrade -y && \
3 | apt-get install -y nodejs \
4 | npm
5 | EXPOSE 5432
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Open Source Labs Beta
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ReadMeUtils/Climb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/ReadMeUtils/Climb.png
--------------------------------------------------------------------------------
/ReadMeUtils/slider.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/ReadMeUtils/slider.gif
--------------------------------------------------------------------------------
/ReadMeUtils/stepLatencyHeat.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/ReadMeUtils/stepLatencyHeat.gif
--------------------------------------------------------------------------------
/ReadMeUtils/steplatencychart.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/ReadMeUtils/steplatencychart.gif
--------------------------------------------------------------------------------
/ReadMeUtils/timePeriodAdjust.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/ReadMeUtils/timePeriodAdjust.gif
--------------------------------------------------------------------------------
/__test__/FlowChartVeiw-test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
2 | import { render, screen } from '@testing-library/react';
3 | import FlowChartView from '../src/DetailedView/FlowChartView.tsx';
4 | import '@testing-library/jest-dom';
5 |
6 | // Mock the external dependencies
7 | vi.mock('@xyflow/react', () => ({
8 | ReactFlow: vi.fn(({ nodes, edges, children }) => (
9 |
10 |
{JSON.stringify(nodes)}
11 |
{JSON.stringify(edges)}
12 | {children}
13 |
14 | )),
15 | Controls: vi.fn(() => Controls
),
16 | Background: vi.fn(() => Background
),
17 | BackgroundVariant: {
18 | Lines: 'lines'
19 | }
20 | }));
21 |
22 | vi.mock('react-redux', () => ({
23 | useSelector: vi.fn(),
24 | Provider: ({ children }) => {children}
25 | }));
26 |
27 | describe('FlowChartView', () => {
28 | let nodes;
29 | let edges;
30 |
31 | // Complex step function definition with parallel states
32 | const complexDefinition = {
33 | StartAt: "InitialCheck",
34 | States: {
35 | "InitialCheck": {
36 | Type: "Choice",
37 | Choices: [
38 | { Next: "ProcessOrder" },
39 | { Next: "HandleError" }
40 | ],
41 | Default: "HandleError"
42 | },
43 | "ProcessOrder": {
44 | Type: "Parallel",
45 | Branches: [
46 | {
47 | StartAt: "ValidatePayment",
48 | States: {
49 | "ValidatePayment": {
50 | Type: "Task",
51 | Next: "ProcessPayment"
52 | },
53 | "ProcessPayment": {
54 | Type: "Task",
55 | End: true
56 | }
57 | }
58 | },
59 | {
60 | StartAt: "CheckInventory",
61 | States: {
62 | "CheckInventory": {
63 | Type: "Task",
64 | Next: "UpdateInventory"
65 | },
66 | "UpdateInventory": {
67 | Type: "Task",
68 | Next: "NotifyWarehouse"
69 | },
70 | "NotifyWarehouse": {
71 | Type: "Task",
72 | End: true
73 | }
74 | }
75 | }
76 | ],
77 | Next: "FinalizeOrder"
78 | },
79 | "HandleError": {
80 | Type: "Task",
81 | Next: "NotifySupport"
82 | },
83 | "NotifySupport": {
84 | Type: "Task",
85 | Catch: [
86 | {
87 | ErrorEquals: ["States.ALL"],
88 | Next: "FinalizeOrder"
89 | }
90 | ],
91 | Next: "FinalizeOrder"
92 | },
93 | "FinalizeOrder": {
94 | Type: "Task",
95 | End: true
96 | }
97 | }
98 | };
99 |
100 | beforeAll(() => {
101 | render(
102 |
107 | );
108 |
109 | const nodesElement = screen.getByTestId('nodes');
110 | const edgesElement = screen.getByTestId('edges');
111 |
112 | nodes = JSON.parse(nodesElement.textContent || '[]');
113 | edges = JSON.parse(edgesElement.textContent || '[]');
114 | });
115 |
116 | describe('Basic Path Tests', () => {
117 | it('creates all expected nodes', () => {
118 | const expectedNodes = [
119 | 'InitialCheck', 'ProcessOrder', 'HandleError',
120 | 'NotifySupport', 'FinalizeOrder', 'ValidatePayment',
121 | 'ProcessPayment', 'CheckInventory', 'UpdateInventory',
122 | 'NotifyWarehouse'
123 | ];
124 |
125 | expect(nodes).toHaveLength(expectedNodes.length);
126 | expectedNodes.forEach(nodeName => {
127 | expect(nodes.some(node => node.data.name === nodeName)).toBe(true);
128 | });
129 | });
130 |
131 | it('verifies simple linear path from HandleError to FinalizeOrder', () => {
132 | const errorPath = edges.filter(edge =>
133 | edge.source === 'HandleError' ||
134 | (edge.source === 'NotifySupport' && edge.target === 'FinalizeOrder')
135 | );
136 |
137 | expect(errorPath).toHaveLength(2);
138 | expect(errorPath[0].source).toBe('HandleError');
139 | expect(errorPath[0].target).toBe('NotifySupport');
140 | expect(errorPath[1].source).toBe('NotifySupport');
141 | expect(errorPath[1].target).toBe('FinalizeOrder');
142 | });
143 | });
144 |
145 | describe('Choice State Tests', () => {
146 | it('creates correct edges for Choice state', () => {
147 | const choiceEdges = edges.filter(edge => edge.source === 'InitialCheck');
148 |
149 | expect(choiceEdges).toHaveLength(2);
150 | expect(choiceEdges.some(edge => edge.target === 'ProcessOrder')).toBe(true);
151 | expect(choiceEdges.some(edge => edge.target === 'HandleError')).toBe(true);
152 | });
153 | });
154 |
155 | describe('Parallel State Tests', () => {
156 | it('verifies parallel state structure', () => {
157 | // Test parallel state connections to start of each branch
158 | const branchStartEdges = edges.filter(edge =>
159 | edge.source === 'ProcessOrder' &&
160 | ['ValidatePayment', 'CheckInventory'].includes(edge.target)
161 | );
162 | expect(branchStartEdges).toHaveLength(2);
163 |
164 | // Test that terminal nodes of parallel branches connect to Next state
165 | const terminalNodeEdges = edges.filter(edge =>
166 | (edge.source === 'ProcessPayment' || edge.source === 'NotifyWarehouse') &&
167 | edge.target === 'FinalizeOrder'
168 | );
169 | expect(terminalNodeEdges).toHaveLength(2);
170 | });
171 |
172 | it('creates correct structure for payment processing branch', () => {
173 | const paymentBranchEdges = edges.filter(edge =>
174 | edge.source === 'ValidatePayment' ||
175 | edge.source === 'ProcessPayment'
176 | );
177 |
178 | expect(paymentBranchEdges).toHaveLength(2); // One for internal connection, one for connection to FinalizeOrder
179 | expect(paymentBranchEdges.some(edge =>
180 | edge.source === 'ValidatePayment' && edge.target === 'ProcessPayment'
181 | )).toBe(true);
182 | expect(paymentBranchEdges.some(edge =>
183 | edge.source === 'ProcessPayment' && edge.target === 'FinalizeOrder'
184 | )).toBe(true);
185 | });
186 |
187 | it('creates correct structure for inventory management branch', () => {
188 | const inventoryBranchEdges = edges.filter(edge =>
189 | ['CheckInventory', 'UpdateInventory', 'NotifyWarehouse'].includes(edge.source)
190 | );
191 |
192 | expect(inventoryBranchEdges).toHaveLength(3); // Two internal connections, one to FinalizeOrder
193 | expect(inventoryBranchEdges.some(edge =>
194 | edge.source === 'CheckInventory' && edge.target === 'UpdateInventory'
195 | )).toBe(true);
196 | expect(inventoryBranchEdges.some(edge =>
197 | edge.source === 'UpdateInventory' && edge.target === 'NotifyWarehouse'
198 | )).toBe(true);
199 | expect(inventoryBranchEdges.some(edge =>
200 | edge.source === 'NotifyWarehouse' && edge.target === 'FinalizeOrder'
201 | )).toBe(true);
202 | });
203 | });
204 |
205 | describe('Error Handling Tests', () => {
206 | it('creates correct edges for Catch conditions', () => {
207 | const catchEdges = edges.filter(edge =>
208 | edge.source === 'NotifySupport' &&
209 | edge.target === 'FinalizeOrder'
210 | );
211 |
212 | expect(catchEdges).toHaveLength(1);
213 | });
214 | });
215 |
216 | });
--------------------------------------------------------------------------------
/conf/defaults.ini:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/conf/defaults.ini
--------------------------------------------------------------------------------
/conf/grafana.ini:
--------------------------------------------------------------------------------
1 | [server]
2 | http_addr = 0.0.0.0
3 | http_port = 3000
4 | allow_origin = http://localhost:5173
5 | allow_origin = *
6 | enable_gzip = true
7 |
8 | [security]
9 | allow_embedding = true
10 | allow_embedding_same_org = true
11 |
12 | [auth.anonymous]
13 | enabled = true
14 | org_role = Viewer
15 |
16 | [log]
17 | mode = console
18 | level = info
19 |
--------------------------------------------------------------------------------
/data/grafana.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/data/grafana.db
--------------------------------------------------------------------------------
/database/docs/2024-10-30-ERD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/database/docs/2024-10-30-ERD.png
--------------------------------------------------------------------------------
/database/docs/20241011171901_create_initial_schema.sql:
--------------------------------------------------------------------------------
1 |
2 | -- up method sql output
3 | create table "step_functions" (
4 | "step_function_id" serial primary key,
5 | "name" varchar(80) not null,
6 | "arn" varchar(2048) not null,
7 | "region" varchar(80) not null,
8 | "type" varchar(10) not null,
9 | "definition" json not null,
10 | "description" varchar(256),
11 | "comment" text,
12 | "has_versions" boolean not null default '0',
13 | "is_version" boolean not null default '0',
14 | "revisionId" varchar(32) not null,
15 | "parent_id" integer);
16 |
17 | alter table "step_functions" add constraint "step_functions_parent_id_foreign"
18 | foreign key ("parent_id")
19 | references "step_functions" ("step_function_id")
20 | on delete CASCADE;
21 |
22 | create table "step_function_aliases" (
23 | "alias_id" serial primary key,
24 | "name" varchar(80) not null,
25 | "arn" varchar(2048) not null,
26 | "region" varchar(80) not null,
27 | "description" varchar(256));
28 |
29 | create table "alias_routes" (
30 | "alias_id" integer not null,
31 | "step_function_id" integer not null,
32 | "weight" smallint not null, constraint
33 | "alias_routes_pkey"
34 | primary key ("alias_id", "step_function_id"));
35 |
36 | alter table "alias_routes" add constraint "alias_routes_alias_id_foreign"
37 | foreign key ("alias_id")
38 | references "step_function_aliases" ("alias_id")
39 | on delete CASCADE;
40 |
41 | alter table "alias_routes"
42 | add constraint "alias_routes_step_function_id_foreign"
43 | foreign key ("step_function_id")
44 | references "step_functions" ("step_function_id")
45 | on delete CASCADE;
46 |
47 | create table "steps" (
48 | "step_id" serial primary key,
49 | "step_function_id" integer not null,
50 | "name" text not null,
51 | "type" varchar(20) not null,
52 | "comment" text);
53 |
54 | alter table "steps" add constraint "steps_step_function_id_foreign"
55 | foreign key ("step_function_id")
56 | references "step_functions" ("step_function_id")
57 | on delete CASCADE;
58 |
59 | create table "step_average_latencies" (
60 | "latency_id" bigserial primary key,
61 | "step_id" integer not null, "average" double precision not null,
62 | "executions" bigint not null,
63 | "start_time" timestamptz not null,
64 | "end_time" timestamptz not null);
65 |
66 | alter table "step_average_latencies" add constraint "step_average_latencies_step_id_foreign"
67 | foreign key ("step_id")
68 | references "steps" ("step_id")
69 | on delete CASCADE;
70 |
71 | create table "step_function_average_latencies" (
72 | "latency_id" bigserial primary key,
73 | "step_function_id" integer not null,
74 | "average" double precision not null,
75 | "executions" bigint not null,
76 | "start_time" timestamptz not null,
77 | "end_time" timestamptz not null);
78 |
79 | alter table "step_function_average_latencies"
80 | add constraint "step_function_average_latencies_step_function_id_foreign"
81 | foreign key ("step_function_id")
82 | references "step_functions" ("step_function_id")
83 | on delete CASCADE;
84 |
85 | create table "step_function_monitoring" (
86 | "monitor_id" serial primary key,
87 | "step_function_id" integer not null,
88 | "newest_update" timestamptz,
89 | "oldest_update" timestamptz,
90 | "start_time" timestamptz,
91 | "end_time" timestamptz,
92 | "active" boolean not null);
93 |
94 | alter table "step_function_monitoring"
95 | add constraint "step_function_monitoring_step_function_id_foreign"
96 | foreign key ("step_function_id")
97 | references "step_functions" ("step_function_id")
98 | on delete CASCADE;
99 |
100 | -- down method sql output
101 | drop table if exists "alias_routes"
102 | drop table if exists "step_function_aliases"
103 | drop table if exists "step_function_monitoring"
104 | drop table if exists "step_function_average_latencies"
105 | drop table if exists "step_average_latencies"
106 | drop table if exists "step_function_latencies"
107 | drop table if exists "step_latencies"
108 | drop table if exists "steps"
109 | drop table if exists "step_functions";
--------------------------------------------------------------------------------
/database/docs/knex-migration-usage.md:
--------------------------------------------------------------------------------
1 | # Knex Migrations
2 |
3 | https://knexjs.org/
4 |
5 | Knex helps to document and automate database schema changes and data migrations over time.
6 |
7 | ## Getting Started
8 |
9 | ### Creating and Updating the database time_climb
10 |
11 |
12 | - Create an .env file in the root folder directory. Make sure it is ignored by
13 | git.
14 |
15 | - Add the following environment variables to .env:
16 |
17 | PGHOST='localhost'
(whatever address postgres is listening on)
18 | PGPORT='5432'
(whatever port postgres is listening on)
19 | PGUSER='postgres'
(whatever your username is)
20 | PGPASSWORD='your_users_password'
21 |
22 |
23 | npm run setup:database
to create the initial database called time_climb
.
24 |
25 | npm run migrate:latest
to update the database with the latest schema.
26 |
27 |
28 | Updating to the latest migration will typically erase all data in your database
29 | unless the migration script specifically is set up to migrate your data, which
30 | it currently is not.
31 |
32 | ### Populating with Seed Data
33 |
34 | `npm run seed:data` will populate the database with test data. This will erase any existing data you might have in your database.
35 |
36 | ## Knex Detailed Usage
37 |
38 | Convience scripts are set up in package.json to run common knex migration functions.
39 |
40 | - `npm run migrate:make` Creating a migration with npm run migate:make
41 | - `npm run migrate:latest` Updating the database with the latest schema changes
42 | - `npm run migrate:rollback` Rolling back a change to the database
43 |
44 | These are partially necessary because both the configuration files and migrations are written in TypeScript. Knex's command line tool does not natively support TypeScript. The need for these tools might become unnecessary with proper TypeScript configuration.
45 |
46 | ### Creating a migration
47 |
48 | Creating a new migration will place a new file in /database/migrations
49 |
50 | You can pass the name of the file after the script like so:
51 |
52 | `npm run migrate:make -- your_migration_file_name`
53 |
54 | ### Updatating the database to the latest schema changes
55 |
56 | `npm run migrate:latest`
57 |
58 | This will update the database with latest schema changes. If necessary its possible to also migrate data so that it data is not lost, which might be especially helpful in a production environment.
59 |
60 | ### Rolling back a migration
61 |
62 | `npm run migrate:rollback`
63 |
64 | This will roll back only the very latest migration unit. You can pass in an argument `--all` to rollback all of the completed migrations this way:
65 |
66 | `npm run migrate:rollback -- --all`
67 |
--------------------------------------------------------------------------------
/database/migrations/20241011171901_create_initial_schema.ts:
--------------------------------------------------------------------------------
1 | import type { Knex } from "knex";
2 |
3 | export async function up(knex: Knex): Promise {
4 | // step_functions table
5 | await knex.schema.createTable("step_functions", (table) => {
6 | table.increments("step_function_id").notNullable();
7 | table.string("name", 80).notNullable();
8 | table.string("arn", 2048).notNullable();
9 | table.string("region", 80).notNullable();
10 | table.string("type", 10).notNullable();
11 | table.json("definition").notNullable();
12 | table.string("description", 256);
13 | table.text("comment");
14 | table.boolean("has_versions").notNullable().defaultTo(false);
15 | table.boolean("is_version").notNullable().defaultTo(false);
16 | table.string("revision_id");
17 | table.integer("parent_id").unsigned();
18 | table
19 | .foreign("parent_id")
20 | .references("step_function_id")
21 | .inTable("step_functions")
22 | .onDelete("CASCADE");
23 | });
24 |
25 | // steps table
26 | await knex.schema.createTable("steps", (table) => {
27 | table.increments("step_id").notNullable();
28 | table.integer("step_function_id").unsigned().notNullable();
29 | table.text("name").notNullable();
30 | table.string("type", 20).notNullable();
31 | table.text("comment");
32 | table
33 | .foreign("step_function_id")
34 | .references("step_function_id")
35 | .inTable("step_functions")
36 | .onDelete("CASCADE");
37 | });
38 | // step_latencies table
39 | await knex.schema.createTable("step_average_latencies", (table) => {
40 | table.bigIncrements("latency_id").notNullable();
41 | table.integer("step_id").unsigned().notNullable();
42 | table.double("average").notNullable();
43 | table.bigInteger("executions").unsigned().notNullable();
44 | table.timestamp("start_time", { useTz: true }).notNullable();
45 | table.timestamp("end_time", { useTz: true }).notNullable();
46 | table
47 | .foreign("step_id")
48 | .references("step_id")
49 | .inTable("steps")
50 | .onDelete("CASCADE");
51 | });
52 | // step_function_latencies table
53 | await knex.schema.createTable("step_function_average_latencies", (table) => {
54 | table.bigIncrements("latency_id").notNullable();
55 | table.integer("step_function_id").unsigned().notNullable();
56 | table.double("average").notNullable();
57 | table.bigInteger("executions").unsigned().notNullable();
58 | table.timestamp("start_time", { useTz: true }).notNullable();
59 | table.timestamp("end_time", { useTz: true }).notNullable();
60 | table
61 | .foreign("step_function_id")
62 | .references("step_function_id")
63 | .inTable("step_functions")
64 | .onDelete("CASCADE");
65 | });
66 | // step_function_trackers table
67 | await knex.schema.createTable("step_function_trackers", (table) => {
68 | table.increments("tracker_id").notNullable();
69 | table.integer("step_function_id").unsigned().notNullable();
70 | table.timestamp("newest_execution_time", { useTz: true });
71 | table.timestamp("oldest_execution_time", { useTz: true });
72 | table.timestamp("tracker_start_time", { useTz: true });
73 | table.timestamp("tracker_end_time", { useTz: true });
74 | table.boolean("active").defaultTo(true).notNullable();
75 | table.string("log_group_arn");
76 | table
77 | .foreign("step_function_id")
78 | .references("step_function_id")
79 | .inTable("step_functions")
80 | .onDelete("CASCADE");
81 | });
82 | }
83 |
84 | export async function down(knex: Knex): Promise {
85 | await knex.schema.dropTableIfExists("alias_routes");
86 | await knex.schema.dropTableIfExists("incomplete_streams");
87 | await knex.schema.dropTableIfExists("incomplete_executions");
88 | await knex.schema.dropTableIfExists("step_function_aliases");
89 | await knex.schema.dropTableIfExists("step_function_monitoring");
90 | await knex.schema.dropTableIfExists("step_function_monitors");
91 |
92 | await knex.schema.dropTableIfExists("step_function_trackers");
93 | await knex.schema.dropTableIfExists("step_average_latencies");
94 | await knex.schema.dropTableIfExists("step_function_average_latencies");
95 | await knex.schema.dropTableIfExists("step_function_latencies");
96 | await knex.schema.dropTableIfExists("step_latencies");
97 | await knex.schema.dropTableIfExists("steps");
98 | await knex.schema.dropTableIfExists("step_functions");
99 | }
100 |
--------------------------------------------------------------------------------
/database/seeds/01_step_functions.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import { Knex } from "knex";
3 | import definitions from "./utils/step-function-definitions";
4 | import { StepFunctionsTable } from "../../server/models/types";
5 |
6 | export async function seed(knex: Knex): Promise {
7 | // Deletes ALL existing entries
8 | await knex("step_functions").del();
9 |
10 | // Inserts seed entries
11 | await knex("step_functions").insert([
12 | {
13 | step_function_id: 1,
14 | name: "CallbackExample",
15 | arn: "arn:aws:states:us-east-1:123456789012:stateMachine:CallBackExample",
16 | region: "us-east-1",
17 | type: "STANDARD",
18 | definition: definitions[0],
19 | comment:
20 | "An example of the Amazon States Language for starting a task and waiting for a callback.",
21 | has_versions: false,
22 | is_version: false,
23 | revision_id: "e3f0c4c8a0b503b8059f2b9f876bcc27",
24 | },
25 | {
26 | step_function_id: 2,
27 | name: "HelloWorld",
28 | arn: "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld",
29 | region: "us-east-1",
30 | type: "STANDARD",
31 | definition: definitions[1],
32 | comment:
33 | "A Hello World example demonstrating various state types of the Amazon States Language. It is composed of flow control states only, so it does not need resources to run.",
34 | has_versions: false,
35 | is_version: false,
36 | revision_id: "afq0c4c8a0b503b8059f2b9f876egg56",
37 | },
38 | ]);
39 |
40 | await knex.raw(`
41 | SELECT setval('step_functions_step_function_id_seq', 2, true);
42 | `);
43 | }
44 |
--------------------------------------------------------------------------------
/database/seeds/02_steps.ts:
--------------------------------------------------------------------------------
1 | import { Knex } from "knex";
2 | import { StepsTable } from "../../server/models/types";
3 |
4 | export async function seed(knex: Knex): Promise {
5 | // Deletes ALL existing entries
6 | await knex("steps").del();
7 |
8 | // Inserts seed entries
9 | await knex("steps").insert([
10 | //CallbackExample step function steps
11 | {
12 | step_id: 1,
13 | step_function_id: 1,
14 | name: "Start Task And Wait For Callback",
15 | type: "Task",
16 | },
17 | {
18 | step_id: 2,
19 | step_function_id: 1,
20 | name: "Notify Success",
21 | type: "Task",
22 | },
23 | {
24 | step_id: 3,
25 | step_function_id: 1,
26 | name: "Notify Failure",
27 | type: "Task",
28 | },
29 | // begin of HelloWord step function steps
30 | {
31 | step_id: 4,
32 | step_function_id: 2,
33 | name: "Pass",
34 | type: "Pass",
35 | },
36 | {
37 | step_id: 5,
38 | step_function_id: 2,
39 | name: "Hello World example?",
40 | type: "Choice",
41 | },
42 | {
43 | step_id: 6,
44 | step_function_id: 2,
45 | name: "Yes",
46 | type: "Pass",
47 | },
48 | {
49 | step_id: 7,
50 | step_function_id: 2,
51 | name: "No",
52 | type: "Fail",
53 | },
54 | {
55 | step_id: 8,
56 | step_function_id: 2,
57 | name: "Wait 3 sec",
58 | type: "Wait",
59 | },
60 | {
61 | step_id: 9,
62 | step_function_id: 2,
63 | name: "Parallel State",
64 | type: "Parallel",
65 | },
66 | {
67 | step_id: 10,
68 | step_function_id: 2,
69 | name: "Hello",
70 | type: "Pass",
71 | },
72 | {
73 | step_id: 11,
74 | step_function_id: 2,
75 | name: "World",
76 | type: "Pass",
77 | },
78 | {
79 | step_id: 12,
80 | step_function_id: 2,
81 | name: "Hello World",
82 | type: "Pass",
83 | },
84 | ]);
85 |
86 | await knex.raw(`
87 | SELECT setval('steps_step_id_seq', 12, true);
88 | `);
89 | }
90 |
--------------------------------------------------------------------------------
/database/seeds/03_latencies.ts:
--------------------------------------------------------------------------------
1 | import { Knex } from "knex";
2 | import { StepTimeData } from "./utils/types";
3 | import latenciesGenerator from "./utils/latenciesGenerator";
4 | export async function seed(knex: Knex): Promise {
5 | // Deletes ALL existing entries
6 | await knex("step_average_latencies").del();
7 | await knex("step_function_average_latencies").del();
8 | // // Inserts seed entries
9 |
10 | const steps: StepTimeData[] = [
11 | {
12 | step_id: 1,
13 | averageRange: 5,
14 | averageOffset: 10,
15 | executionOffset: 5000,
16 | executionRange: 3000,
17 | },
18 | {
19 | step_id: 2,
20 | averageRange: 0.4,
21 | averageOffset: 0.65,
22 | executionRange: 45,
23 | executionOffset: 125,
24 | },
25 | {
26 | step_id: 3,
27 | averageRange: 2.5,
28 | averageOffset: 3,
29 | executionRange: 25,
30 | executionOffset: 50,
31 | },
32 | ];
33 | console.log("Generating latency data");
34 | const data = await latenciesGenerator(steps, 1);
35 | console.log("Data created");
36 | const batchSize = 1000;
37 | try {
38 | await knex.transaction(async (transaction) => {
39 | console.log("Inserting data in batches");
40 | await knex
41 | .batchInsert("step_average_latencies", data.step_latencies, batchSize)
42 | .transacting(transaction);
43 | console.log(
44 | `Inserted ${data.step_latencies.length} rows in step_latences`
45 | );
46 | await knex
47 | .batchInsert(
48 | "step_function_average_latencies",
49 | data.step_function_latencies,
50 | batchSize
51 | )
52 | .transacting(transaction);
53 | console.log(
54 | `Inserted ${data.step_function_latencies.length} rows in step_function_latences`
55 | );
56 | });
57 |
58 | const helloWorldSteps = [
59 | {
60 | step_id: 4,
61 | averageRange: 0.25,
62 | averageOffset: 0.001,
63 | executionOffset: 50,
64 | executionRange: 30,
65 | },
66 | {
67 | step_id: 5,
68 | averageRange: 0.8,
69 | averageOffset: 0.2,
70 | executionOffset: 50,
71 | executionRange: 3000,
72 | },
73 | {
74 | step_id: 6,
75 | averageRange: 1,
76 | averageOffset: 0.05,
77 | executionOffset: 50,
78 | executionRange: 30,
79 | },
80 | {
81 | step_id: 7,
82 | averageRange: 3,
83 | averageOffset: 0.5,
84 | executionOffset: 5000,
85 | executionRange: 3000,
86 | },
87 | {
88 | step_id: 8,
89 | averageRange: 1,
90 | averageOffset: 20,
91 | executionOffset: 5000,
92 | executionRange: 3000,
93 | },
94 | {
95 | step_id: 9,
96 | averageRange: 10,
97 | averageOffset: 2,
98 | executionOffset: 50,
99 | executionRange: 30,
100 | },
101 | {
102 | step_id: 10,
103 | averageRange: 4.5,
104 | averageOffset: 0.25,
105 | executionOffset: 50,
106 | executionRange: 30,
107 | },
108 | {
109 | step_id: 11,
110 | averageRange: 2,
111 | averageOffset: 0.2,
112 | executionOffset: 50,
113 | executionRange: 30,
114 | },
115 | {
116 | step_id: 12,
117 | averageRange: 0.3,
118 | averageOffset: 0.001,
119 | executionOffset: 50,
120 | executionRange: 30,
121 | },
122 | ];
123 |
124 | const data2 = await latenciesGenerator(helloWorldSteps, 2);
125 | console.log("Data created");
126 |
127 | await knex.transaction(async (transaction) => {
128 | console.log("Inserting data in batches");
129 | await knex
130 | .batchInsert("step_average_latencies", data2.step_latencies, batchSize)
131 | .transacting(transaction);
132 | console.log(
133 | `Inserted ${data2.step_latencies.length} rows in step_latences`
134 | );
135 | await knex
136 | .batchInsert(
137 | "step_function_average_latencies",
138 | data2.step_function_latencies,
139 | batchSize
140 | )
141 | .transacting(transaction);
142 | console.log(
143 | `Inserted ${data2.step_function_latencies.length} rows in step_function_latences`
144 | );
145 | });
146 | } catch (error) {
147 | console.log(`Error inserting latency data: ${error}`);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/database/seeds/04_trackers.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import { Knex } from "knex";
3 |
4 | export async function seed(knex: Knex): Promise {
5 | // Deletes ALL existing entries
6 | await knex("step_function_trackers").del();
7 | }
8 |
--------------------------------------------------------------------------------
/database/seeds/utils/latenciesGenerator.ts:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 | import {
3 | StepTimeData,
4 | StepAverageLatenciesTable,
5 | StepFunctionAverageLatenciesTable,
6 | } from "./types";
7 |
8 | const getRandomNumber = (range: number, offset: number): number => {
9 | return Math.random() * range + offset;
10 | };
11 |
12 | /**
13 | * This function is designed to generate random latency data for steps_latencies
14 | * and step_function_latencies tables. The data is random, but with a
15 | * designated range and offset. Future versions could generate a sine wave
16 | * pattern if random is less visually interesting. Generating data in this way
17 | * lets it be random, yet have a predictable average.
18 | *
19 | * @param events array of StepTimeData objects, which have paremets to tweak the
20 | * random numbers generated
21 | * @param stepFunctionId the id of the step function, as stored in the
22 | * step_functions_table
23 | * @returns Object of {step_latencies: StepLatenciesTable[],
24 | * step_function_latencies: StepFunctionLatenciesTablep[]}
25 | */
26 |
27 | interface LatencyData {
28 | step_latencies: StepAverageLatenciesTable[];
29 | step_function_latencies: StepFunctionAverageLatenciesTable[];
30 | }
31 |
32 | const latenciesGenerator = async (
33 | events: StepTimeData[],
34 | stepFunctionId: number
35 | ): Promise => {
36 | const data: LatencyData = { step_latencies: [], step_function_latencies: [] };
37 |
38 | const now = moment.utc();
39 | const oneYearAgo = moment.utc().subtract(1, "year");
40 |
41 | while (now.isAfter(oneYearAgo)) {
42 | const startOfCurrentHour = now.clone().utc().startOf("hour");
43 | const endOfCurrentHour = now.clone().utc().endOf("hour");
44 | let totalDuration = 0;
45 | const maxExecutions: number[] = [];
46 |
47 | for (const event of events) {
48 | const average = getRandomNumber(event.averageRange, event.averageOffset);
49 | const executions = getRandomNumber(
50 | event.executionRange,
51 | event.executionOffset
52 | );
53 | data.step_latencies.push({
54 | step_id: event.step_id,
55 | average,
56 | executions: Math.floor(executions),
57 | start_time: startOfCurrentHour.toISOString(),
58 | end_time: endOfCurrentHour.toISOString(),
59 | });
60 |
61 | totalDuration += average;
62 | maxExecutions.push(executions);
63 | }
64 | data.step_function_latencies.push({
65 | step_function_id: stepFunctionId,
66 | average: totalDuration,
67 | executions: Math.floor(Math.max(...maxExecutions)),
68 | start_time: startOfCurrentHour.toISOString(),
69 | end_time: endOfCurrentHour.toISOString(),
70 | });
71 |
72 | now.subtract(1, "hour");
73 | }
74 | return data;
75 | };
76 |
77 | export default latenciesGenerator;
78 |
--------------------------------------------------------------------------------
/database/seeds/utils/step-function-definitions.ts:
--------------------------------------------------------------------------------
1 | const definitions = [
2 | //callback example step function
3 | `{
4 | "Comment": "An example of the Amazon States Language for starting a task and waiting for a callback.",
5 | "StartAt": "Start Task And Wait For Callback",
6 | "States": {
7 | "Start Task And Wait For Callback": {
8 | "Type": "Task",
9 | "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
10 | "Parameters": {
11 | "QueueUrl": "https://sqs.REGION.amazonaws.com/ACCOUNT_ID/MyQueue",
12 | "MessageBody": {
13 | "MessageTitle": "Task started by Step Functions. Waiting for callback with task token.",
14 | "TaskToken.$": "$$.Task.Token"
15 | }
16 | },
17 | "Next": "Notify Success",
18 | "Catch": [
19 | {
20 | "ErrorEquals": [
21 | "States.ALL"
22 | ],
23 | "Next": "Notify Failure"
24 | }
25 | ]
26 | },
27 | "Notify Success": {
28 | "Type": "Task",
29 | "Resource": "arn:aws:states:::sns:publish",
30 | "Parameters": {
31 | "Message": "Callback received. Task started by Step Functions succeeded.",
32 | "TopicArn": "arn:PARTITION:sns:REGION:ACCOUNT_NUMBER:MySnsTopic"
33 | },
34 | "End": true
35 | },
36 | "Notify Failure": {
37 | "Type": "Task",
38 | "Resource": "arn:aws:states:::sns:publish",
39 | "Parameters": {
40 | "Message": "Task started by Step Functions failed.",
41 | "TopicArn": "arn:PARTITION:sns:REGION:ACCOUNT_NUMBER:MySnsTopic"
42 | },
43 | "End": true
44 | }
45 | }
46 | }`,
47 | // hello world step function
48 | `{
49 | "Comment": "A Hello World example demonstrating various state types of the Amazon States Language. It is composed of flow control states only, so it does not need resources to run.",
50 | "StartAt": "Pass",
51 | "States": {
52 | "Pass": {
53 | "Comment": "A Pass state passes its input to its output, without performing work. They can also generate static JSON output, or transform JSON input using filters and pass the transformed data to the next state. Pass states are useful when constructing and debugging state machines.",
54 | "Type": "Pass",
55 | "Result": {
56 | "IsHelloWorldExample": true
57 | },
58 | "Next": "Hello World example?"
59 | },
60 | "Hello World example?": {
61 | "Comment": "A Choice state adds branching logic to a state machine. Choice rules can implement many different comparison operators, and rules can be combined using And, Or, and Not",
62 | "Type": "Choice",
63 | "Choices": [
64 | {
65 | "Variable": "$.IsHelloWorldExample",
66 | "BooleanEquals": true,
67 | "Next": "Yes"
68 | },
69 | {
70 | "Variable": "$.IsHelloWorldExample",
71 | "BooleanEquals": false,
72 | "Next": "No"
73 | }
74 | ],
75 | "Default": "Yes"
76 | },
77 | "Yes": {
78 | "Type": "Pass",
79 | "Next": "Wait 3 sec"
80 | },
81 | "No": {
82 | "Type": "Fail",
83 | "Cause": "Not Hello World"
84 | },
85 | "Wait 3 sec": {
86 | "Comment": "A Wait state delays the state machine from continuing for a specified time.",
87 | "Type": "Wait",
88 | "Seconds": 3,
89 | "Next": "Parallel State"
90 | },
91 | "Parallel State": {
92 | "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.",
93 | "Type": "Parallel",
94 | "Next": "Hello World",
95 | "Branches": [
96 | {
97 | "StartAt": "Hello",
98 | "States": {
99 | "Hello": {
100 | "Type": "Pass",
101 | "End": true
102 | }
103 | }
104 | },
105 | {
106 | "StartAt": "World",
107 | "States": {
108 | "World": {
109 | "Type": "Pass",
110 | "End": true
111 | }
112 | }
113 | }
114 | ]
115 | },
116 | "Hello World": {
117 | "Type": "Pass",
118 | "End": true
119 | }
120 | }
121 | }`,
122 | // hello test step function
123 | `{
124 | "Comment": "A Hello Test example demonstrating various state types of the Amazon States Language. It is composed of flow control states only, so it does not need resources to run.",
125 | "StartAt": "Pass",
126 | "States": {
127 | "Pass": {
128 | "Comment": "A Pass state passes its input to its output, without performing work. They can also generate static JSON output, or transform JSON input using filters and pass the transformed data to the next state. Pass states are useful when constructing and debugging state machines.",
129 | "Type": "Pass",
130 | "Next": "Hello Test example?"
131 | },
132 | "Hello Test example?": {
133 | "Comment": "A Choice state adds branching logic to a state machine. Choice rules can implement many different comparison operators, and rules can be combined using And, Or, and Not",
134 | "Type": "Choice",
135 | "Choices": [
136 | {
137 | "Variable": "$.IsHelloTestExample",
138 | "BooleanEquals": true,
139 | "Next": "Yes"
140 | },
141 | {
142 | "Variable": "$.IsHelloTestExample",
143 | "BooleanEquals": false,
144 | "Next": "No"
145 | }
146 | ],
147 | "Default": "Yes"
148 | },
149 | "Yes": {
150 | "Type": "Pass",
151 | "Next": "Wait 3 sec"
152 | },
153 | "No": {
154 | "Type": "Fail",
155 | "Cause": "IsHelloTestExample was false"
156 | },
157 | "Wait 3 sec": {
158 | "Comment": "A Wait state delays the state machine from continuing for a specified time.",
159 | "Type": "Wait",
160 | "Seconds": 3,
161 | "Next": "Parallel State"
162 | },
163 | "Parallel State": {
164 | "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.",
165 | "Type": "Parallel",
166 | "Next": "Hello Test",
167 | "Branches": [
168 | {
169 | "StartAt": "Hello",
170 | "States": {
171 | "Hello": {
172 | "Type": "Pass",
173 | "End": true
174 | }
175 | }
176 | },
177 | {
178 | "StartAt": "Test",
179 | "States": {
180 | "Test": {
181 | "Type": "Pass",
182 | "End": true
183 | }
184 | }
185 | }
186 | ]
187 | },
188 | "Hello Test": {
189 | "Type": "Pass",
190 | "End": true
191 | }
192 | }
193 | }`,
194 | ];
195 |
196 | export default definitions;
197 |
--------------------------------------------------------------------------------
/database/seeds/utils/types.ts:
--------------------------------------------------------------------------------
1 | // file for describing the shape of the database tables in order to help
2 | // ensure that we are building and using models correctly
3 |
4 | export interface StepFunctionsTable {
5 | step_function_id?: number;
6 | name: string;
7 | arn: string;
8 | region: string;
9 | type: string;
10 | definition: string;
11 | description?: string | null;
12 | comment?: string | null;
13 | has_versions: boolean;
14 | is_version: boolean;
15 | revision_id?: string;
16 | log_group?: string;
17 | parent_id?: number | null;
18 | }
19 |
20 | export interface StepsTable {
21 | step_id?: number;
22 | step_function_id: number;
23 | name: string;
24 | type: string;
25 | comment?: string | null;
26 | }
27 |
28 | export interface StepAverageLatenciesTable {
29 | latency_id?: number;
30 | step_id: number;
31 | average: number;
32 | executions: number;
33 | start_time: string;
34 | end_time: string;
35 | }
36 |
37 | export interface StepFunctionAverageLatenciesTable {
38 | latency_id?: number;
39 | step_function_id: number;
40 | average: number;
41 | executions: number;
42 | start_time: string;
43 | end_time: string;
44 | }
45 |
46 | export interface StepFunctionMonitoringTable {
47 | id: number;
48 | step_function_id: number;
49 | newest_update: string;
50 | oldest_update: string;
51 | start_time: string;
52 | end_time?: string | null;
53 | active?: boolean;
54 | }
55 |
56 | export interface StepFunctionAliasesTable {
57 | alias_id: number;
58 | name: string;
59 | arn: string;
60 | region: string;
61 | description?: string;
62 | }
63 |
64 | export interface AliasRoutesTable {
65 | alias_id: number;
66 | step_function_id: number;
67 | weight: number;
68 | }
69 |
70 | export interface StepTimeData {
71 | step_id: number;
72 | averageRange: number;
73 | averageOffset: number;
74 | executionOffset: number;
75 | executionRange: number;
76 | }
77 |
--------------------------------------------------------------------------------
/database/setupDatabase.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import pg from 'pg';
3 | const { Client } = pg;
4 |
5 | async function setupDatabase() {
6 | const client = new Client({
7 | database: 'postgres',
8 | host: process.env.PGHOST,
9 | port: 5432,
10 | user: process.env.PGUSER,
11 | password: process.env.PGPASSWORD,
12 | });
13 | try {
14 | await client.connect();
15 | const query = "SELECT 1 FROM pg_database WHERE datname = 'time_climb';";
16 | const result = await client.query(query);
17 |
18 | // if no results create the database
19 | if (result.rowCount === 0) {
20 | console.log(`Creating database: time_climb`);
21 | await client.query(
22 | //LOCALE_PROVIDER = 'libc'
23 | /*ENCODING = 'UTF8'
24 | LC_COLLATE = 'C'
25 | LC_CTYPE = 'C'*/
26 | `CREATE DATABASE time_climb
27 | WITH
28 | TABLESPACE = pg_default
29 | IS_TEMPLATE = False;`
30 | );
31 | console.log('Database created');
32 | } else {
33 | console.log(`Database time_climb already exists`);
34 | }
35 | } catch (err) {
36 | console.log(
37 | `Error setting up database time_climb: ${err} ${process.env.PGPASSWORD} ${process.env.PGUSER} ${process.env.PGHOST}`
38 | );
39 | } finally {
40 | await client.end();
41 | }
42 | }
43 |
44 | setupDatabase();
45 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | image: postgres:latest
4 | container_name: tc-db
5 | environment:
6 | - POSTGRES_USER=postgres
7 | - POSTGRES_PASSWORD=admin
8 | - POSTGRES_HOST=localhost
9 | - POSTGRES_PORT=5432
10 | - POSTGRES_DB=postgres
11 | ports:
12 | - '5432:5432'
13 | app:
14 | depends_on:
15 | - db
16 | build: .
17 | environment:
18 | - PGUSER=postgres
19 | - PGPASSWORD=admin
20 | - PGHOST=db
21 | - PGPORT=5432
22 | - AWS_ACCESS_KEY_ID=[INSERT_AWS_KEY]
23 | - AWS_SECRET_ACCESS_KEY=[INSERT_AWS_SECRET_KEY]
24 | - AWS_REGION=[INSERT_YOUR_AWS_REGION]
25 | ports:
26 | - 3000:3000
27 | command: npm run create:server
28 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import globals from 'globals';
3 | import reactHooks from 'eslint-plugin-react-hooks';
4 | import reactRefresh from 'eslint-plugin-react-refresh';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: {
15 | ...globals.browser,
16 | ...globals.mocha,
17 | ...globals.jest,
18 | },
19 | },
20 | plugins: {
21 | 'react-hooks': reactHooks,
22 | 'react-refresh': reactRefresh,
23 | },
24 | rules: {
25 | ...reactHooks.configs.recommended.rules,
26 | 'react-refresh/only-export-components': [
27 | 'warn',
28 | { allowConstantExport: true },
29 | ],
30 | },
31 | }
32 | );
33 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Time Climb
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/knexfile.ts:
--------------------------------------------------------------------------------
1 | // configuratin file for knex cli tool
2 | import "dotenv/config";
3 | import type { Knex } from "knex";
4 |
5 | const development: Knex.Config = {
6 | client: "pg",
7 | connection: {
8 | host: process.env.PGHOST,
9 | port: Number(process.env.PGPORT),
10 | user: process.env.PGUSER,
11 | password: process.env.PGPASSWORD,
12 | database: "time_climb",
13 | },
14 | migrations: {
15 | directory: "./database/migrations",
16 | extension: "ts",
17 | },
18 | seeds: {
19 | directory: "./database/seeds",
20 | },
21 | pool: { min: 2, max: 10 }, // can be optimized later, these are default values
22 | debug: true,
23 | };
24 |
25 | const production: Knex.Config = {
26 | client: "pg",
27 | connection: process.env.DATABASE_URL,
28 | migrations: {
29 | directory: "./database/migrations",
30 | extension: "ts",
31 | },
32 | pool: { min: 2, max: 10 }, // can be optimized later, these are default values
33 | debug: false,
34 | };
35 |
36 | const knexCliConfig: { [key: string]: Knex.Config } = {
37 | development,
38 | production,
39 | };
40 | export default knexCliConfig;
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "time-climb",
3 | "private": true,
4 | "version": "1.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "test": "vitest --coverage",
9 | "lint": "eslint .",
10 | "preview": "vite preview",
11 | "build:server": "tsc -b tsconfig.server.json",
12 | "start": "node dist/server/server.js",
13 | "dev:server": "tsx watch server/server.ts",
14 | "setup:database": "npx tsx ./database/setupDatabase",
15 | "migrate:make": "npx tsx ./node_modules/.bin/knex migrate:make",
16 | "migrate:latest": "npx tsx ./node_modules/.bin/knex migrate:latest",
17 | "migrate:rollback": "npx tsx ./node_modules/.bin/knex migrate:rollback",
18 | "seed:make": "npx tsx ./node_modules/.bin/knex seed:make",
19 | "seed:run": "npx tsx ./node_modules/.bin/knex seed:run",
20 | "create:server": "npx tsx ./database/setupDatabase && npx tsx ./node_modules/.bin/knex migrate:latest && node dist/server/server.js"
21 | },
22 | "dependencies": {
23 | "@aws-sdk/client-cloudwatch-logs": "^3.675.0",
24 | "@aws-sdk/client-sfn": "^3.670.0",
25 | "@aws-sdk/credential-providers": "^3.670.0",
26 | "@dagrejs/dagre": "^1.1.4",
27 | "@headlessui/react": "^2.2.0",
28 | "@heroicons/react": "^2.1.5",
29 | "@reduxjs/toolkit": "^2.2.8",
30 | "@xyflow/react": "^12.3.1",
31 | "bottleneck": "^2.19.5",
32 | "chart.js": "^4.4.5",
33 | "cors": "^2.8.5",
34 | "dotenv": "^16.4.5",
35 | "express": "^4.21.1",
36 | "knex": "^3.1.0",
37 | "moment": "^2.30.1",
38 | "moment-timezone": "^0.5.46",
39 | "node-cron": "^3.0.3",
40 | "pg": "^8.13.0",
41 | "plotly.js-dist": "^2.35.2",
42 | "react": "^18.3.1",
43 | "react-dom": "^18.3.1",
44 | "react-redux": "^9.1.2",
45 | "react-router-dom": "^6.27.0",
46 | "redux": "^5.0.1",
47 | "redux-persist": "^6.0.0",
48 | "ts-node-dev": "^2.0.0",
49 | "vite-express": "^0.19.0"
50 | },
51 | "devDependencies": {
52 | "@eslint/js": "^9.11.1",
53 | "@testing-library/jest-dom": "^6.5.0",
54 | "@testing-library/react": "^16.0.1",
55 | "@types/cors": "^2.8.17",
56 | "@types/express": "^5.0.0",
57 | "@types/node": "^22.7.5",
58 | "@types/react": "^18.3.10",
59 | "@types/react-dom": "^18.3.0",
60 | "@vitejs/plugin-react-swc": "^3.5.0",
61 | "autoprefixer": "^10.4.20",
62 | "@vitest/coverage-v8": "^2.1.4",
63 | "eslint": "^9.11.1",
64 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
65 | "eslint-plugin-react-refresh": "^0.4.12",
66 | "globals": "^15.9.0",
67 | "jsdom": "^25.0.1",
68 | "postcss": "^8.4.47",
69 | "tailwindcss": "^3.4.14",
70 | "ts-node": "^10.9.2",
71 | "tsx": "^4.19.1",
72 | "typescript": "^5.5.3",
73 | "typescript-eslint": "^8.7.0",
74 | "vite": "^5.4.8",
75 | "vitest": "^2.1.2"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/controllers/api/stepFunctionsApiController.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response, NextFunction } from "express";
2 | import stepFunctionsModel from "../../models/stepFunctionsModel";
3 | import type { StepFunctionSubset } from "../../models/types";
4 | const getStepFunctions = async (
5 | req: Request,
6 | res: Response,
7 | next: NextFunction
8 | ):Promise => {
9 | const stepFunctions: StepFunctionSubset[]= await stepFunctionsModel.selectAllStepFunctions();
10 | res.locals.stepFunctions = stepFunctions;
11 | return next();
12 | };
13 |
14 | const stepFunctionsApiController = {
15 | getStepFunctions,
16 | };
17 |
18 | export default stepFunctionsApiController;
19 |
--------------------------------------------------------------------------------
/server/controllers/aws/getStepFunctionAWSController.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | // const require('dotenv').config()
3 | import moment, { Moment } from "moment";
4 | import {
5 | SFNClient,
6 | DescribeStateMachineCommand,
7 | LoggingConfiguration,
8 | } from "@aws-sdk/client-sfn";
9 | import {
10 | CloudWatchLogsClient,
11 | DescribeLogGroupsCommand,
12 | DescribeLogGroupsCommandInput,
13 | } from "@aws-sdk/client-cloudwatch-logs";
14 | import { fromEnv } from "@aws-sdk/credential-providers";
15 | import stepFunctionsModel from "../../models/stepFunctionsModel";
16 | import type { Request, Response, NextFunction } from "express";
17 | import stepsModel from "../../models/stepsModel";
18 | import parseStepFunction from "../../utils/parseStepFunction";
19 | import { NewStepRow } from "../../models/types";
20 | import stepFunctionTrackersModel from "../../models/stepFunctionTrackersModel";
21 |
22 | /**
23 | * Gets the creation time for a log group. This helps determine how far back to
24 | * query logs for step function executions.
25 | *
26 | * @param {string} logGroupName The log group name to filter results by,
27 | * without the full arn
28 | * @returns {Promise}Promise that resolves to a number,
29 | * which is an epoch time in milliseconds, or undefined
30 | */
31 | const getLogGroupCreationTime = async (
32 | logGroupName: string,
33 | region: string
34 | ): Promise => {
35 | const client = new CloudWatchLogsClient({
36 | region,
37 | credentials: fromEnv(),
38 | });
39 |
40 | const params: DescribeLogGroupsCommandInput = {
41 | logGroupNamePrefix: logGroupName,
42 | };
43 |
44 | const command = new DescribeLogGroupsCommand(params);
45 | const response = await client.send(command);
46 | console.log("DescribeLogGroupsCommand Response", response);
47 | const creationTime = response?.logGroups[0]?.creationTime;
48 | return creationTime;
49 | };
50 |
51 | const getLoggingConfiguration = async (
52 | config: LoggingConfiguration
53 | ): Promise => {
54 | let logGroupArn = "";
55 | let logGroupName = "";
56 | for (const logs of config.destinations) {
57 | if (logs.cloudWatchLogsLogGroup.logGroupArn) {
58 | logGroupArn = logs.cloudWatchLogsLogGroup.logGroupArn;
59 | logGroupName = logGroupArn.split("log-group:")[1];
60 | if (logGroupName.endsWith(":*")) logGroupName = logGroupName.slice(0, -2);
61 | break;
62 | }
63 | }
64 | return [logGroupArn, logGroupName];
65 | };
66 |
67 | const getStepFunctionAWS = async (
68 | req: Request,
69 | res: Response,
70 | next: NextFunction
71 | ) => {
72 | try {
73 | const stateMachineArn = req.body.arn;
74 | //first check it state machine exists in database
75 | const result = await stepFunctionsModel.checkStepFunctionsTable(
76 | stateMachineArn
77 | );
78 | if (result) {
79 | res.locals.newTable = {
80 | name: result.name,
81 | step_function_id: result.step_function_id,
82 | definition: result.definition,
83 | };
84 | return next();
85 | }
86 | //otherwise, retrieve state machine from AWS, add it to database, and retrieve from database
87 | const describeStateMachine = new DescribeStateMachineCommand({
88 | stateMachineArn,
89 | });
90 |
91 | // arn has region after 3rd ':'
92 | const region: string = stateMachineArn.split(":")[3];
93 | const sfn = new SFNClient({
94 | region,
95 | credentials: fromEnv(),
96 | });
97 | console.log('env', process.env)
98 | const response = await sfn.send(describeStateMachine);
99 |
100 | const [logGroupArn, logGroupName] = await getLoggingConfiguration(
101 | response.loggingConfiguration
102 | );
103 |
104 | if (logGroupArn.length <= 0 || logGroupName.length <= 0) {
105 | return next("No log group found for step function");
106 | }
107 |
108 | const logGroupCreationTime = await getLogGroupCreationTime(
109 | logGroupName,
110 | region
111 | );
112 |
113 | // limit max log ingestion to one week ago
114 | const creationDate = moment(logGroupCreationTime).startOf("hour").utc();
115 | const oneWeekAgo = moment().subtract(1, "week").startOf("hour").utc();
116 |
117 | const newerDate = oneWeekAgo.isBefore(creationDate)
118 | ? creationDate
119 | : oneWeekAgo;
120 |
121 | const addStepFunction = await stepFunctionsModel.addToStepFunctionTable(
122 | response,
123 | region
124 | );
125 |
126 | const aslObject = JSON.parse(response.definition);
127 | const stepRows: NewStepRow[] = await parseStepFunction(
128 | aslObject,
129 | addStepFunction.step_function_id
130 | );
131 |
132 | await stepsModel.insertSteps(stepRows);
133 |
134 | const { tracker_id } = await stepFunctionTrackersModel.insertTracker({
135 | step_function_id: addStepFunction.step_function_id,
136 | log_group_arn: logGroupArn,
137 | tracker_start_time: newerDate.toISOString(),
138 | active: true,
139 | });
140 | res.locals.trackerId = tracker_id;
141 | console.log("tracker_id", tracker_id);
142 | res.locals.newTable = addStepFunction;
143 | return next();
144 | } catch (error) {
145 | return next(error);
146 | }
147 | };
148 |
149 | export default getStepFunctionAWS;
150 |
--------------------------------------------------------------------------------
/server/controllers/client/clientController.ts:
--------------------------------------------------------------------------------
1 | // controllers specific to the client could go in this folder
2 |
--------------------------------------------------------------------------------
/server/controllers/stepFunctionController.ts:
--------------------------------------------------------------------------------
1 | import Knex from 'knex';
2 | const pg = Knex({
3 | client: 'pg',
4 | //needs to be moved to env
5 | connection: 'postgresql://postgres:Dudeman32%211@localhost:5432/time_climb',
6 | });
7 | import { Request, Response, NextFunction } from 'express';
8 | import 'dotenv/config';
9 | import {
10 | SFNClient,
11 | ListStateMachinesCommand,
12 | ListStateMachinesCommandOutput,
13 | ListStateMachineVersionsCommand,
14 | ListStateMachineVersionsCommandOutput,
15 | DescribeStateMachineCommand,
16 | } from '@aws-sdk/client-sfn';
17 | import { fromEnv } from '@aws-sdk/credential-providers';
18 |
19 | //connection to credientials
20 | const sfn = new SFNClient({
21 | region: process.env.AWS_REGION,
22 | credentials: fromEnv(),
23 | });
24 |
25 | // type Next = (?Function) => void | Promise
26 |
27 | //make alias innerface create custom
28 | interface stepFunctionController {
29 | listStateMachines: (
30 | req: Request,
31 | res: Response,
32 | next: NextFunction
33 | ) => void | Promise;
34 | }
35 |
36 | const stepFunctionController: stepFunctionController = {
37 | //make request for all state machines from AWS
38 | listStateMachines: async function (
39 | req: Request,
40 | res: Response,
41 | next: NextFunction
42 | ) {
43 | const listStateMachines = new ListStateMachinesCommand();
44 | try {
45 | const response = await sfn.send(listStateMachines);
46 | console.log('listSTateMachines response', response);
47 | //iterate through statemachines array
48 | //for each machine,
49 | //get it's details and create an object with properties in database
50 | response.stateMachines.map((mach) => {
51 | async function getStateMachineDetails(
52 | stateMachineArn: string
53 | ): Promise {
54 | // const describeStateMachine = getStateMachineDetails({
55 | // stateMachineArn,
56 | // });
57 | }
58 | });
59 | return next();
60 | } catch (error) {
61 | return next(error);
62 | }
63 | },
64 | };
65 |
66 | // stepFunctionController.hotdog(req, res, next)
67 |
68 | //getting state machines from AWS
69 | // stepFunctionController.
70 | export default stepFunctionController;
71 |
--------------------------------------------------------------------------------
/server/docs/api.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | ## Usage
4 |
5 | ## Base URL
6 | ### **`http://localhost:3000/api`**
7 |
8 | All API requests are be made to http://localhost:3000/api
9 |
10 | ## Endpoints
11 |
12 | ### To retrieve **Step Functions** from database
13 |
14 |
15 |
16 | GET
/
Gets all step functions previously stored in the database
17 |
18 |
19 | #### Parameters
20 |
21 | > None
22 |
23 | #### Responses
24 | > | http code | content-type | response |
25 | > | --------- | -------------------------------- | -------- |
26 | > | `200` | `application/json;charset=UTF-8` | JSON |
27 |
28 | #### Example Status Code for 200 Ok
29 |
30 | ```json
31 | [
32 | {
33 | "step_function_id": 0,
34 | "name": "string",
35 | "description": "string",
36 | "definition": {}
37 | }
38 | ]
39 | ```
40 |
41 |
42 |
43 |
44 | POST
/step_functions/addStepFunction
45 | Adds a step function to the database
46 |
47 |
48 |
49 | #### Parameters
50 |
51 | > | name | type | data type | description |
52 | > | ---- | -------- | --------- | ---------------------------------------------------- |
53 | > | body | required | object | the arn that corresponds to a specific state machine | |
54 |
55 | #### Example Body - JSON
56 |
57 | ```json
58 | { "arn": "arn:partition:service:region:account-id:resource-type:resource-id" }
59 | ```
60 |
61 | ### Responses
62 |
63 | > | http code | content-type | response |
64 | > | --------- | -------------------------------- | -------- |
65 | > | `200` | `application/json;charset=UTF-8` | JSON |
66 |
67 | #### Example Response for 200 Ok: Returns the newly added step function
68 |
69 | ```json
70 | {
71 | "step_function_id": 0,
72 | "name": "string",
73 | "definition": {}
74 | }
75 | ```
76 |
77 |
78 |
79 |
80 | GET
/step_functions/:step_functions_id/hours
81 | Retrieves hourly average latencies over a span of one day in ascending order
82 |
83 |
84 | #### Parameters
85 |
86 | > | name | type | data type | description |
87 | > | ------------------------| -------- | --------- | ---------------------------------------- |
88 | > | `path.step_function_id` | required | string | The unique ID associated with this step function in database passed in the URL path (`/:step_function_id/hours`) |
89 |
90 | #### Example Request
91 | localhost:3000/api/average-latencies/:step_function_id/hours
92 |
93 | ### Responses
94 |
95 | > | http code | content-type | response |
96 | > | --------- | -------------------------------- | -------- |
97 | > | `200` | `application/json;charset=UTF-8` | JSON |
98 |
99 | #### Example Response for 200 Ok
100 | ##### Note: If a step function's latencies are not found in database, the elements value in the response will be an empty object
101 | ```json
102 | [
103 | {
104 | "date": "2024-10-23T04:00:00.000Z",
105 | "stepFunctionAverageLatency": 18.144353388646543,
106 | "steps": {
107 | "Start Task And Wait For Callback": {
108 | "average": 14.44404914949289
109 | },
110 | "Notify Success": {
111 | "average": 0.6627415526268704
112 | },
113 | "Notify Failure": {
114 | "average": 3.037562686526782
115 | }
116 | }
117 | }
118 | ]
119 | ```
120 |
121 |
122 |
123 |
124 | GET
/step_functions/:step_functions_id/days
125 | Retrieves daily average latencies over a span of 7 days in ascending order
126 |
127 |
128 | #### Parameters
129 |
130 | > | name | type | data type | description |
131 | > | ------------------------| -------- | --------- | ---------------------------------------- |
132 | > | `path.step_function_id` | required | string | The unique ID associated with this step function in database passed in the URL path (`/:step_function_id/days`) |
133 |
134 | #### Example Request
135 | localhost:3000/api/average-latencies/:step_function_id/days
136 |
137 | ### Responses
138 |
139 | > | http code | content-type | response |
140 | > | --------- | -------------------------------- | -------- |
141 | > | `200` | `application/json;charset=UTF-8` | JSON |
142 |
143 | #### Example Response for 200 Ok
144 | ##### Note: If a step function's latencies are not found in database, the elements value in the response will be an empty object
145 | ```json
146 | [
147 | {
148 | "date": "2024-10-23T04:00:00.000Z",
149 | "stepFunctionAverageLatency": 18.144353388646543,
150 | "steps": {
151 | "Start Task And Wait For Callback": {
152 | "average": 14.44404914949289
153 | },
154 | "Notify Success": {
155 | "average": 0.6627415526268704
156 | },
157 | "Notify Failure": {
158 | "average": 3.037562686526782
159 | }
160 | }
161 | }
162 | ]
163 | ```
164 |
165 |
166 |
167 | GET
/step_functions/:step_functions_id/weeks
168 | Retrieves weekly average latencies over a span of 12 weeks in ascending order
169 |
170 |
171 | #### Parameters
172 |
173 | > | name | type | data type | description |
174 | > | ------------------------| -------- | --------- | ---------------------------------------- |
175 | > | `path.step_function_id` | required | string | The unique ID associated with this step function in database passed in the URL path (`/:step_function_id/weeks`) |
176 |
177 | #### Example Request
178 | localhost:3000/api/average-latencies/:step_function_id/weeks
179 |
180 | ### Responses
181 |
182 | > | http code | content-type | response |
183 | > | --------- | -------------------------------- | -------- |
184 | > | `200` | `application/json;charset=UTF-8` | JSON |
185 |
186 | #### Example Response for 200 Ok
187 | ##### Note: If a step function's latencies are not found in database, the elements value in the response will be an empty object
188 | ```json
189 | [
190 | {
191 | "date": "2024-08-12T04:00:00.000Z",
192 | "stepFunctionAverageLatency": 18.144353388646543,
193 | "steps": {
194 | "Start Task And Wait For Callback": {
195 | "average": 14.44404914949289
196 | },
197 | "Notify Success": {
198 | "average": 0.6627415526268704
199 | },
200 | "Notify Failure": {
201 | "average": 3.037562686526782
202 | }
203 | }
204 | }
205 | ]
206 | ```
207 |
208 |
209 |
210 |
211 | GET
/step_functions/:step_functions_id/months
212 | Retrieves monthly average latencies over a span of 12 months in ascending order
213 |
214 |
215 | #### Parameters
216 |
217 | > | name | type | data type | description |
218 | > | ------------------------| -------- | --------- | ---------------------------------------- |
219 | > | `path.step_function_id` | required | string | The unique ID associated with this step function in database passed in the URL path (`/:step_function_id/months`) |
220 |
221 | #### Example Request
222 | localhost:3000/api/average-latencies/:step_function_id/months
223 |
224 | ### Responses
225 |
226 | > | http code | content-type | response |
227 | > | --------- | -------------------------------- | -------- |
228 | > | `200` | `application/json;charset=UTF-8` | JSON |
229 |
230 | #### Example Response for 200 Ok
231 | ##### Note: If a step function's latencies are not found in database, the elements value in the response will be an empty object
232 | ```json
233 | [
234 | {
235 | "date": "2023-11-01T04:00:00.000Z",
236 | "stepFunctionAverageLatency": 18.144353388646543,
237 | "steps": {
238 | "Start Task And Wait For Callback": {
239 | "average": 14.44404914949289
240 | },
241 | "Notify Success": {
242 | "average": 0.6627415526268704
243 | },
244 | "Notify Failure": {
245 | "average": 3.037562686526782
246 | }
247 | }
248 | }
249 | ]
250 | ```
--------------------------------------------------------------------------------
/server/models/averageLatenciesModel.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import { AverageLatencies, StepFunctionAverageLatenciesTable } from "./types";
3 |
4 | //hourly latencies
5 | const getStepFunctionLatencies = async (
6 | step_function_id: number,
7 | start_time: string,
8 | end_time: string
9 | ): Promise => {
10 | try {
11 | const latenciesObj = await db(
12 | "step_function_average_latencies"
13 | )
14 | .select("step_function_id", "average", "start_time")
15 | .whereBetween("start_time", [start_time, end_time])
16 | .andWhere("step_function_id", step_function_id)
17 | .orderBy("start_time", "asc");
18 | return latenciesObj;
19 | } catch (err) {
20 | console.log(`Error getting latencies for step function: ${err}`);
21 | }
22 | };
23 |
24 | //daily latencies
25 | const getStepFunctionLatenciesDaily = async (
26 | step_function_id: number,
27 | start_time: string,
28 | end_time: string
29 | ): Promise => {
30 | try {
31 | const latenciesObj = await db(
32 | "step_function_average_latencies"
33 | )
34 | .select(db.raw("DATE(start_time) AS start_time"))
35 | .avg("average AS average")
36 | .whereBetween("start_time", [start_time, end_time])
37 | .where("step_function_id", step_function_id)
38 | .groupBy(db.raw("DATE(start_time)"))
39 | .orderBy("start_time");
40 | return latenciesObj;
41 | } catch (err) {
42 | console.log(`Error getting latencies for step function: ${err}`);
43 | }
44 | };
45 | //weekly latencies
46 | const getStepFunctionLatenciesWeekly = async (
47 | step_function_id: number,
48 | start_time: string,
49 | end_time: string
50 | ): Promise => {
51 | try {
52 | const latenciesObj = await db(
53 | "step_function_average_latencies"
54 | )
55 | .select(db.raw("DATE_TRUNC('week', \"start_time\") as start_time"))
56 | .avg("average AS average")
57 | .whereBetween("start_time", [start_time, end_time])
58 | .where("step_function_id", step_function_id)
59 | .groupBy(db.raw("DATE_TRUNC('week', \"start_time\")"))
60 | .orderBy("start_time");
61 | return latenciesObj;
62 | } catch (err) {
63 | return err;
64 | }
65 | };
66 | //monthly latencies
67 | const getStepFunctionLatenciesMonthly = async (
68 | step_function_id: number,
69 | start_time: string,
70 | end_time: string
71 | ): Promise => {
72 | try {
73 | const latenciesObj = await db(
74 | "step_function_average_latencies"
75 | )
76 | .select(db.raw("DATE_TRUNC('month', \"start_time\") AS start_time"))
77 | .avg("average AS average")
78 | .whereBetween("start_time", [start_time, end_time])
79 | .where("step_function_id", step_function_id)
80 | .groupBy(db.raw("DATE_TRUNC('month', \"start_time\")"))
81 | .orderBy("start_time");
82 | console.log(latenciesObj);
83 | return latenciesObj;
84 | } catch (err) {
85 | return err;
86 | }
87 | };
88 |
89 | const averageLatenciesModel = {
90 | getStepFunctionLatencies,
91 | getStepFunctionLatenciesDaily,
92 | getStepFunctionLatenciesWeekly,
93 | getStepFunctionLatenciesMonthly,
94 | };
95 |
96 | export default averageLatenciesModel;
97 |
--------------------------------------------------------------------------------
/server/models/db.ts:
--------------------------------------------------------------------------------
1 | // file for connecting to postgres with knex / pg
2 | // then exporting that connection where necessary
3 | import knex from "knex";
4 | import knexDbConfig from "./dbConfig";
5 |
6 | const db = knex(knexDbConfig);
7 |
8 | export default db;
9 |
--------------------------------------------------------------------------------
/server/models/dbConfig.ts:
--------------------------------------------------------------------------------
1 | // configuratin file for knex cli tool
2 | import "dotenv/config";
3 | import type { Knex } from "knex";
4 |
5 | const knexDbConfig: Knex.Config = {
6 | client: "pg",
7 | connection: {
8 | host: process.env.PGHOST,
9 | port: Number(process.env.PGPORT),
10 | user: process.env.PGUSER,
11 | password: process.env.PGPASSWORD,
12 | database: "time_climb",
13 | },
14 | pool: { min: 2, max: 10 }, // can be optimized later, these are default values
15 | // debug: true,
16 | };
17 |
18 | export default knexDbConfig;
19 |
--------------------------------------------------------------------------------
/server/models/stepAverageLatenciesModel.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import type { StepAverageLatenciesTable } from "./types";
3 | import type { StepAverageLatencies } from "./types";
4 |
5 | //getting hourly latencies (no averages)
6 | const getHourlyLatenciesBetweenTimes = async (
7 | stepIds: number[],
8 | startTime: string,
9 | endTime: string
10 | ): Promise => {
11 | try {
12 | const rows = await db("step_average_latencies")
13 | .select("latency_id", "step_id", "average", "start_time", "executions")
14 | .whereIn("step_id", stepIds)
15 | .whereBetween("start_time", [startTime, endTime])
16 | .orderBy(["start_time", "step_id"]);
17 | return rows;
18 | } catch (err) {
19 | console.log(`Error getting step lantency data between times: ${err}`);
20 | return [];
21 | }
22 | };
23 |
24 | const getDailyLatencyAveragesBetweenTimes = async (
25 | stepIds: number[],
26 | startTime: string,
27 | endTime: string
28 | ): Promise => {
29 | try {
30 | const rows = await db("step_average_latencies")
31 | .select("step_id")
32 | .select(db.raw("DATE_TRUNC('day', \"start_time\") AS start_time"))
33 | .avg("average AS average")
34 | .whereIn("step_id", stepIds)
35 | .whereBetween("start_time", [startTime, endTime])
36 | .groupBy(db.raw("step_id, DATE_TRUNC('day', \"start_time\")"))
37 | .orderBy(["start_time", "step_id"]);
38 | // console.log(rows)
39 | return rows;
40 | } catch (err) {
41 | console.log(`Error getting step lantency data between times: ${err}`);
42 | return [];
43 | }
44 | };
45 |
46 | const getWeeklyLatencyAveragesBetweenTimes = async (
47 | stepIds: number[],
48 | startTime: string,
49 | endTime: string
50 | ): Promise => {
51 | try {
52 | const rows = await db("step_average_latencies")
53 | .select("step_id")
54 | .select(db.raw("DATE_TRUNC('week', \"start_time\") AS start_time"))
55 | .avg("average as average")
56 | .whereIn("step_id", stepIds)
57 | .whereBetween("start_time", [startTime, endTime])
58 | .groupBy(db.raw("step_id, DATE_TRUNC('week', \"start_time\")"))
59 | .orderBy(["start_time", "step_id"]);
60 | // console.log(rows)
61 | return rows;
62 | } catch (err) {
63 | console.log(`Error getting step lantency data between times: ${err}`);
64 | return [];
65 | }
66 | };
67 |
68 | const getMonthlyLatencyAveragesBetweenTimes = async (
69 | stepIds: number[],
70 | startTime: string,
71 | endTime: string
72 | ): Promise => {
73 | try {
74 | const rows = await db("step_average_latencies")
75 | .select("step_id")
76 | .select(db.raw("DATE_TRUNC('month', \"start_time\") AS start_time"))
77 | .avg("average AS average")
78 | .whereIn("step_id", stepIds)
79 | .whereBetween("start_time", [startTime, endTime])
80 | .groupBy(db.raw("step_id, DATE_TRUNC('month', \"start_time\")"))
81 | .orderBy(["start_time", "step_id"]);
82 | return rows;
83 | } catch (err) {
84 | console.log(`Error gettting step latency between times: ${err}`);
85 | return [];
86 | }
87 | };
88 |
89 | const insertStepAverageLatencies = async (
90 | rows: StepAverageLatenciesTable[]
91 | ): Promise => {
92 | try {
93 | await db("step_average_latencies").insert(rows);
94 | } catch (err) {
95 | console.log(`Error inserting rows into Step Averge Latencies: ${err}`);
96 | }
97 | };
98 |
99 | const updateStepAverageLatency = async (
100 | latencyId: number,
101 | average: number,
102 | executions: number
103 | ): Promise => {
104 | try {
105 | await db("step_average_latencies")
106 | .update({ average, executions })
107 | .where("latency_id", latencyId);
108 | } catch (err) {
109 | console.log(`Error updating row in step average latencies: ${err}`);
110 | }
111 | };
112 |
113 | const stepAverageLatenciesModel = {
114 | getHourlyLatenciesBetweenTimes,
115 | getDailyLatencyAveragesBetweenTimes,
116 | getWeeklyLatencyAveragesBetweenTimes,
117 | getMonthlyLatencyAveragesBetweenTimes,
118 | insertStepAverageLatencies,
119 | updateStepAverageLatency,
120 | };
121 |
122 | export default stepAverageLatenciesModel;
123 |
--------------------------------------------------------------------------------
/server/models/stepFunctionAverageLatenciesModel.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import moment from "moment";
3 | import { AverageLatencies, StepFunctionAverageLatenciesTable } from "./types";
4 |
5 | //hourly latencies
6 | const getStepFunctionLatencies = async (
7 | step_function_id: number,
8 | start_time: string,
9 | end_time: string
10 | ): Promise => {
11 | try {
12 | const latenciesObj = await db(
13 | "step_function_average_latencies"
14 | )
15 | .select(
16 | "latency_id",
17 | "step_function_id",
18 | "average",
19 | "start_time",
20 | "executions"
21 | )
22 | .whereBetween("start_time", [start_time, end_time])
23 | .andWhere("step_function_id", step_function_id)
24 | .orderBy("start_time", "asc");
25 | return latenciesObj;
26 | } catch (err) {
27 | console.log(`Error getting latencies for step function: ${err}`);
28 | }
29 | };
30 |
31 | //daily latencies
32 | const getStepFunctionLatenciesDaily = async (
33 | step_function_id: number,
34 | start_time: string,
35 | end_time: string
36 | ): Promise => {
37 | try {
38 | const latenciesObj = await db(
39 | "step_function_average_latencies"
40 | )
41 | .select(db.raw("DATE(start_time) AS start_time"))
42 | .avg("average AS average")
43 | .whereBetween("start_time", [start_time, end_time])
44 | .where("step_function_id", step_function_id)
45 | .groupBy(db.raw("DATE(start_time)"))
46 | .orderBy("start_time");
47 | return latenciesObj;
48 | } catch (err) {
49 | console.log(`Error getting latencies for step function: ${err}`);
50 | }
51 | };
52 | //weekly latencies
53 | const getStepFunctionLatenciesWeekly = async (
54 | step_function_id: number,
55 | start_time: string,
56 | end_time: string
57 | ): Promise => {
58 | try {
59 | const latenciesObj = await db(
60 | "step_function_average_latencies"
61 | )
62 | .select(db.raw("DATE_TRUNC('week', \"start_time\") as start_time"))
63 | .avg("average AS average")
64 | .whereBetween("start_time", [start_time, end_time])
65 | .where("step_function_id", step_function_id)
66 | .groupBy(db.raw("DATE_TRUNC('week', \"start_time\")"))
67 | .orderBy("start_time");
68 | return latenciesObj;
69 | } catch (err) {
70 | return err;
71 | }
72 | };
73 | //monthly latencies
74 | const getStepFunctionLatenciesMonthly = async (
75 | step_function_id: number,
76 | start_time: string,
77 | end_time: string
78 | ): Promise => {
79 | try {
80 | const latenciesObj = await db(
81 | "step_function_average_latencies"
82 | )
83 | .select(db.raw("DATE_TRUNC('month', \"start_time\") AS start_time"))
84 | .avg("average AS average")
85 | .whereBetween("start_time", [start_time, end_time])
86 | .where("step_function_id", step_function_id)
87 | .groupBy(db.raw("DATE_TRUNC('month', \"start_time\")"))
88 | .orderBy("start_time");
89 | return latenciesObj;
90 | } catch (err) {
91 | return err;
92 | }
93 | };
94 |
95 | const insertStepFunctionLatencies = async (
96 | rows: StepFunctionAverageLatenciesTable[]
97 | ) => {
98 | try {
99 | await db(
100 | "step_function_average_latencies"
101 | ).insert(rows);
102 | } catch (err) {
103 | console.log(
104 | `Error inserting rows into step function average latencies: ${err}`
105 | );
106 | return err;
107 | }
108 | };
109 |
110 | const updateStepFunctionLatency = async (
111 | latencyId: number,
112 | average: number,
113 | executions: number
114 | ) => {
115 | try {
116 | await db(
117 | "step_function_average_latencies"
118 | )
119 | .update({ average, executions })
120 | .where("latency_id", latencyId);
121 | } catch (err) {
122 | console.log(
123 | `Error updating row in step function average latencies: ${err}`
124 | );
125 | }
126 | };
127 |
128 | const stepFunctionAverageLatenciesModel = {
129 | getStepFunctionLatencies,
130 | getStepFunctionLatenciesDaily,
131 | getStepFunctionLatenciesWeekly,
132 | getStepFunctionLatenciesMonthly,
133 | insertStepFunctionLatencies,
134 | updateStepFunctionLatency,
135 | };
136 |
137 | export default stepFunctionAverageLatenciesModel;
138 |
--------------------------------------------------------------------------------
/server/models/stepFunctionTrackersModel.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import type {
3 | TrackerStepFunctionsJoinTable,
4 | StepFunctionsTable,
5 | StepFunctionTrackersTable,
6 | NewTrackerRowResult,
7 | } from "./types";
8 |
9 | const getAllTrackerDataWithNames = async (): Promise<
10 | TrackerStepFunctionsJoinTable[]
11 | > => {
12 | try {
13 | const rows = await db(
14 | "step_function_trackers as t"
15 | )
16 | .join(
17 | "step_functions as sf",
18 | "sf.step_function_id",
19 | "t.step_function_id"
20 | )
21 | .select("t.*", "sf.name");
22 | return rows;
23 | } catch (err) {
24 | console.log("Error getting tracker data:", err);
25 | }
26 | };
27 |
28 | const getTrackerDataWithName = async (
29 | trackerId: number
30 | ): Promise => {
31 | try {
32 | const rows: TrackerStepFunctionsJoinTable[] =
33 | await db("step_function_trackers as t")
34 | .join(
35 | "step_functions as sf",
36 | "sf.step_function_id",
37 | "t.step_function_id"
38 | )
39 | .select("t.*", "sf.name")
40 | .where("tracker_id", trackerId);
41 | return rows;
42 | } catch (err) {
43 | console.log("Error getting tracker data:", err);
44 | }
45 | };
46 |
47 | /**
48 | * Updates a tracker table newest_execution_time column to a new date.
49 | * The query is structured to only update if the date is greater than the
50 | * existing date. In knex, we used a function within "andWhere" in order to
51 | * also correctly deal with null values if this is the first time the tracker
52 | * has been run.
53 | * @param trackerId Database primary key of the tracker row
54 | * @param newTime The time to update the row with
55 | * @returns Number of rows updated
56 | */
57 | const updateNewestExecutionTime = async (
58 | trackerId: number,
59 | newTime: string
60 | ): Promise => {
61 | try {
62 | const numberOfRows = db("step_function_trackers")
63 | .update({ newest_execution_time: newTime })
64 | .where("tracker_id", trackerId)
65 | .andWhere(function () {
66 | this.where("newest_execution_time", "<", newTime).orWhereNull(
67 | "newest_execution_time"
68 | );
69 | });
70 | return numberOfRows;
71 | } catch (err) {
72 | console.log(`Error updating newest exec time in tracker table: ${err}`);
73 | }
74 | };
75 |
76 | /**
77 | * Updates a tracker table oldest_execution_time column to a new date.
78 | * The query is structured to only update if the date is less than the
79 | * existing date. In knex, we used a function within "andWhere" in order to
80 | * also correctly deal with null values if this is the first time the tracker
81 | * has been run.
82 | * @param trackerId Database primary key of the tracker row
83 | * @param newTime The time to update the row with
84 | * @returns Number of rows updated
85 | */
86 | const updateOldestExecutionTime = async (
87 | trackerId: number,
88 | newTime: string
89 | ): Promise => {
90 | try {
91 | const numberOfRows = db("step_function_trackers")
92 | .update({ oldest_execution_time: newTime })
93 | .where("tracker_id", trackerId)
94 | .andWhere(function () {
95 | this.where("oldest_execution_time", ">", newTime).orWhereNull(
96 | "oldest_execution_time"
97 | );
98 | });
99 | return numberOfRows;
100 | } catch (err) {
101 | console.log(`Error updating newest exec time in tracker table: ${err}`);
102 | }
103 | };
104 |
105 | const insertTracker = async (
106 | row: StepFunctionTrackersTable
107 | ): Promise => {
108 | try {
109 | const [rowInserted]: NewTrackerRowResult[] =
110 | await db("step_function_trackers")
111 | .insert(row)
112 | .returning("tracker_id");
113 | return rowInserted;
114 | } catch (err) {
115 | console.log(`Error inserting tracker ${err}`);
116 | }
117 | };
118 |
119 | const stepFunctionTrackersModel = {
120 | getAllTrackerDataWithNames,
121 | getTrackerDataWithName,
122 | updateNewestExecutionTime,
123 | updateOldestExecutionTime,
124 | insertTracker,
125 | };
126 |
127 | export default stepFunctionTrackersModel;
128 |
--------------------------------------------------------------------------------
/server/models/stepFunctionsModel.ts:
--------------------------------------------------------------------------------
1 | import db from "./db";
2 | import { DescribeStateMachineOutput } from "@aws-sdk/client-sfn";
3 | import type { StepFunctionsTable } from "./types";
4 |
5 | const selectAllStepFunctions = async () => {
6 | try {
7 | const rows = await db("step_functions").select(
8 | "step_function_id",
9 | "name",
10 | "definition",
11 | "description"
12 | );
13 | return rows;
14 | } catch (err) {
15 | console.log("Error:", err);
16 | return;
17 | }
18 | };
19 |
20 | /**
21 | * Inserts a step function into the step_functions table
22 | * @param stepFunction Response object from DescribeStateMachine command, which
23 | * holds step function data
24 | * @param region The region the step function is hosted within
25 | * @returns Object with step_function_id, name, and definition properties of the
26 | * step function that was inserted into the database.
27 | */
28 | const addToStepFunctionTable = async (
29 | stepFunction: DescribeStateMachineOutput,
30 | region: string
31 | ) => {
32 | try {
33 | const [rowInserted] = await db("step_functions")
34 | .insert({
35 | name: stepFunction.name,
36 | arn: stepFunction.stateMachineArn,
37 | region,
38 | type: stepFunction.type,
39 | definition: stepFunction.definition,
40 | description: stepFunction.description,
41 | revision_id: stepFunction.revisionId,
42 | })
43 | .returning(["step_function_id", "name", "definition"]);
44 |
45 | return rowInserted;
46 | } catch (error) {
47 | console.log("Error inserting step function into database:", error);
48 | return;
49 | }
50 | };
51 |
52 | //check if step function exists in step_functions table in DB
53 | const checkStepFunctionsTable = async (
54 | arn: string
55 | ): Promise => {
56 | const result = await db("step_functions")
57 | .where({ arn: arn })
58 | .first();
59 | return result;
60 | };
61 |
62 | const stepFunctionsModel = {
63 | selectAllStepFunctions,
64 | addToStepFunctionTable,
65 | checkStepFunctionsTable,
66 | };
67 |
68 | export default stepFunctionsModel;
69 |
--------------------------------------------------------------------------------
/server/models/stepsModel.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import db from "./db";
3 | import { NewStepRow, StepsByStepFunctionId, StepsTable } from "./types";
4 |
5 | const getStepsByStepFunctionId = async (
6 | stepFunctionId: number
7 | ): Promise => {
8 | try {
9 | const rows = await db("steps")
10 | .select("step_id", "name", "type", "comment")
11 | .where("step_function_id", stepFunctionId)
12 | .orderBy("step_id");
13 | return rows;
14 | } catch (err) {
15 | console.log(
16 | `Error getting steps for step_function_id ${stepFunctionId}: ${err}`
17 | );
18 | }
19 | };
20 |
21 | const getStepsByStepFunctionIds = async (
22 | stepFunctionIds: number[]
23 | ): Promise => {
24 | try {
25 | const rows = await db("steps")
26 | .select("step_id", "name", "type", "step_function_id")
27 | .whereIn("step_function_id", stepFunctionIds)
28 | .orderBy("step_function_id", "step_id");
29 | return rows;
30 | } catch (err) {
31 | console.log(
32 | `Error getting steps for step_function_ids ${stepFunctionIds}: ${err}`
33 | );
34 | }
35 | };
36 |
37 | /**
38 | * Insert steps into the database for a step function
39 | * @param {NewStepRow[]} rows Steps to insert into the steps table
40 | * @returns Promise (undefined)
41 | */
42 | const insertSteps = async (rows: NewStepRow[]): Promise => {
43 | try {
44 | await db("steps").insert(rows);
45 | } catch (err) {
46 | console.log(`Error inserting steps into steps table: ${err}`);
47 | }
48 | };
49 |
50 | const stepsModel = {
51 | getStepsByStepFunctionId,
52 | getStepsByStepFunctionIds,
53 | insertSteps,
54 | };
55 |
56 | export default stepsModel;
57 |
--------------------------------------------------------------------------------
/server/models/types.ts:
--------------------------------------------------------------------------------
1 | // file for describing the shape of the database tables in order to help
2 | // ensure that we are building and using models correctly
3 |
4 | export interface StepFunctionsTable {
5 | step_function_id?: number;
6 | name: string;
7 | arn: string;
8 | region: string;
9 | type: string;
10 | definition: string;
11 | description?: string | null;
12 | comment?: string | null;
13 | has_versions: boolean;
14 | is_version: boolean;
15 | revision_id?: string;
16 | parent_id?: number | null;
17 | }
18 |
19 | export interface StepsTable {
20 | step_id?: number;
21 | step_function_id?: number;
22 | name: string;
23 | type: string;
24 | comment?: string | null;
25 | }
26 |
27 | export interface NewStepRow {
28 | step_function_id: number;
29 | name: string;
30 | type: string;
31 | comment?: string | null;
32 | }
33 |
34 | export interface StepAverageLatenciesTable {
35 | latency_id?: number;
36 | step_id: number;
37 | average: number;
38 | executions: number;
39 | start_time: string;
40 | end_time: string;
41 | }
42 |
43 | export interface StepFunctionAverageLatenciesTable {
44 | latency_id?: number;
45 | step_function_id: number;
46 | average: number;
47 | executions: number;
48 | start_time: string;
49 | end_time: string;
50 | }
51 |
52 | export interface StepFunctionTrackersTable {
53 | tracker_id?: number;
54 | step_function_id: number;
55 | newest_execution_time?: string | null;
56 | oldest_execution_time?: string | null;
57 | tracker_start_time?: string | null;
58 | tracker_end_time?: string | null;
59 | log_group_arn: string;
60 | active?: boolean;
61 | }
62 |
63 | export interface NewTrackerRowResult {
64 | tracker_id: number;
65 | }
66 |
67 | export interface StepFunctionAliasesTable {
68 | alias_id?: number;
69 | name: string;
70 | arn: string;
71 | region: string;
72 | description?: string;
73 | }
74 |
75 | export interface AliasRoutesTable {
76 | alias_id: number;
77 | step_function_id: number;
78 | weight: number;
79 | }
80 |
81 | export interface IncompleteStreamsTable {
82 | stream_id?: number;
83 | step_function_id: number;
84 | stream_name: string;
85 | log_group_arn: string;
86 | }
87 |
88 | export type TrackerStepFunctionsJoinTable = StepFunctionTrackersTable &
89 | StepFunctionsTable;
90 | //for step average latencies from database
91 | export interface StepAverageLatencies {
92 | latency_id?: number;
93 | step_id: number;
94 | average: number;
95 | start_time: string;
96 | executions?: number;
97 | }
98 |
99 | export interface AverageLatencies {
100 | latency_id?: number;
101 | step_function_id: number;
102 | average: number;
103 | start_time: string;
104 | executions?: number;
105 | }
106 |
107 | export type StepsByStepFunctionId = Pick<
108 | StepsTable,
109 | "step_id" | "name" | "type" | "step_function_id" | "comment"
110 | >;
111 |
112 | export interface LatenciesObj {
113 | date: string;
114 | stepFunctionAverageLatency: number;
115 | steps: {
116 | [key: string]: { average?: number };
117 | };
118 | }
119 |
120 | //for stepFunctionsApiController.getStepFunctions
121 | export interface StepFunctionSubset {
122 | step_function_id?: number;
123 | name: string;
124 | definition: string;
125 | description?: string | null;
126 | }
127 |
--------------------------------------------------------------------------------
/server/routes/api/averageLatenciesRouter.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import type { Request, Response } from "express";
3 | import averageLatenciesApiController from "../../controllers/api/averageLatenciesApiController";
4 | const averageLatenciesRouter = express.Router();
5 | //hourly latencies
6 | averageLatenciesRouter.get(
7 | "/:step_function_id/hours",
8 | averageLatenciesApiController.getAverageLatencies,
9 | (req: Request, res: Response): void => {
10 | res.status(200).json(res.locals.latencyAverages);
11 | return;
12 | }
13 | );
14 |
15 | //daily latencies over past week
16 | averageLatenciesRouter.get(
17 | "/:step_function_id/days",
18 | averageLatenciesApiController.getAverageLatenciesDaily,
19 | (req: Request, res: Response): void => {
20 | res.status(200).json(res.locals.dailyAvgs);
21 | }
22 | );
23 |
24 | //weekly latencies over past 12 weeks
25 | averageLatenciesRouter.get(
26 | "/:step_function_id/weeks",
27 | averageLatenciesApiController.getAverageLatenciesWeekly,
28 | (req: Request, res: Response): void => {
29 | res.status(200).json(res.locals.weeklyAvgs);
30 | }
31 | );
32 |
33 | //monthly latencies over past year
34 | averageLatenciesRouter.get(
35 | "/:step_function_id/months",
36 | averageLatenciesApiController.getAverageLatenciesMonthly,
37 | (req: Request, res: Response): void => {
38 | res.status(200).json(res.locals.monthlyAvgs);
39 | }
40 | );
41 |
42 | export default averageLatenciesRouter;
43 |
--------------------------------------------------------------------------------
/server/routes/api/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import stepFunctionsRouter from "./stepFunctionsRouter";
3 | import averageLatenciesRouter from "./averageLatenciesRouter";
4 | const apiRouter = express.Router();
5 |
6 | // routes /api
7 | apiRouter.use("/step-functions", stepFunctionsRouter);
8 | apiRouter.use("/average-latencies", averageLatenciesRouter);
9 |
10 | export default apiRouter;
11 |
--------------------------------------------------------------------------------
/server/routes/api/stepFunctionsRouter.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from "express";
2 | import stepFunctionsApiController from "../../controllers/api/stepFunctionsApiController";
3 | import getStepFunctionAWS from "../../controllers/aws/getStepFunctionAWSController";
4 | import cronJobWorker from "../../../workers/cloudWatchCronJob";
5 | const stepFunctionRouter = express.Router();
6 | // routes /api/step-functions
7 | stepFunctionRouter.get(
8 | "/",
9 | stepFunctionsApiController.getStepFunctions,
10 | async (req: Request, res: Response): Promise => {
11 | res.status(200).json(res.locals.stepFunctions);
12 | return;
13 | }
14 | );
15 |
16 | stepFunctionRouter.post(
17 | "/addStepFunction",
18 | getStepFunctionAWS,
19 | async (req: Request, res: Response): Promise => {
20 | res.status(200).json(res.locals.newTable);
21 | // run the update if a step function was added
22 | if (res.locals.trackerId !== undefined) {
23 | cronJobWorker.runJob(res.locals.trackerId);
24 | }
25 | return;
26 | }
27 | );
28 |
29 | export default stepFunctionRouter;
30 |
--------------------------------------------------------------------------------
/server/routes/client/index.ts:
--------------------------------------------------------------------------------
1 | // import express, { Request, Response } from "express";
2 | // import path from "path";
3 | // import { fileURLToPath } from "url";
4 |
5 | // const __filename = fileURLToPath(import.meta.url);
6 | // const __dirname = path.dirname(__filename);
7 | // const clientRouter = express.Router();
8 |
9 | // clientRouter.use(express.static(path.join(__dirname, "../dist")));
10 | // clientRouter.get("*", (req: Request, res: Response) => {
11 | // res.sendFile(path.join(__dirname, "../dist", "index.html"));
12 | // return;
13 | // });
14 |
15 | // export default clientRouter;
16 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import express from 'express';
3 | import type {
4 | Request,
5 | Response,
6 | NextFunction,
7 | ErrorRequestHandler,
8 | } from 'express';
9 | import cors from 'cors';
10 | import apiRouter from './routes/api/index';
11 | // import clientRouter from './routes/client/index';
12 | import path from 'path';
13 |
14 | const PORT = 3000;
15 | const app = express();
16 |
17 | app.use(cors());
18 | app.use(express.json());
19 | app.use(express.urlencoded({ extended: true }));
20 |
21 | // console.log(path.join(__dirname, '../assets'));
22 |
23 | app.use(express.static('dist'));
24 |
25 | app.get('/', (req: Request, res: Response) => {
26 | return res.status(200).sendFile(path.join(__dirname, '../index.html'));
27 | });
28 |
29 | // API router
30 | app.use('/api', apiRouter);
31 |
32 | // react app
33 | // app.use(clientRouter);
34 |
35 | app.use(
36 | (
37 | err: ErrorRequestHandler,
38 | req: Request,
39 | res: Response,
40 | next: NextFunction
41 | ): void => {
42 | const errObj = {
43 | log: 'Error caught by global error handler',
44 | status: 500,
45 | message: 'Error caught by global error handler',
46 | };
47 | console.log(err);
48 | const newErrObj = Object.assign({}, errObj, err);
49 | res.status(newErrObj.status).json(newErrObj.message);
50 | return;
51 | }
52 | );
53 |
54 | app.listen(PORT, () => {
55 | console.log(`Listening on port: ${PORT}`);
56 | });
57 |
--------------------------------------------------------------------------------
/server/types/stepFunctionDetailsFromAWS.ts:
--------------------------------------------------------------------------------
1 | //creating and exporting type for arn
2 | export type GetStateMachineDetailsFromAWS = string;
--------------------------------------------------------------------------------
/server/types/stepFunctionLatencyAvgsApi.ts:
--------------------------------------------------------------------------------
1 | export interface MonthlyAverage {
2 | month_start: string;
3 | avg: number;
4 | }
--------------------------------------------------------------------------------
/server/types/stepFunctionsApi.ts:
--------------------------------------------------------------------------------
1 | // here we can define types specific to the step functions api
2 |
3 | export interface GetStepFunctionByIdRequest {
4 | step_function_id: number;
5 | }
6 |
7 | export interface GetStepFunctionResponse {
8 | step_function_id: number;
9 | name: string;
10 | description?: string | null;
11 | definition: string;
12 | }
13 |
14 | export interface PostStepFunctionRequest {
15 | arn: string;
16 | }
17 |
18 |
19 |
--------------------------------------------------------------------------------
/server/types/types.ts:
--------------------------------------------------------------------------------
1 | // we use this to store database results by time for faster future lookup
2 | export interface SFLatenciesByTime {
3 | [isoTimeString: string]: {
4 | stepFunctionId: number;
5 | average: number;
6 | executions?: number;
7 | };
8 | }
9 |
10 | // we use this to store database results by time for faster future lookup
11 | export interface StepLatenciesByTime {
12 | [isoTimeString: string]: {
13 | stepId?: number;
14 | average?: number;
15 | executions?: number;
16 | };
17 | }
18 |
19 | // time period strings as defined by moment
20 | export type TimePeriod = "hours" | "days" | "weeks" | "months";
21 |
22 | // api data reponse to average-latencies endpoints
23 | export interface AverageLatenciesResponse {
24 | date?: string;
25 | stepFunctionAverageLatency?: number;
26 | steps?: {
27 | [stepName: string]: {
28 | average?: number;
29 | executions?: number;
30 | };
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/server/utils/listStateMachines.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import {
3 | SFNClient,
4 | ListStateMachinesCommand,
5 | ListStateMachinesCommandOutput,
6 | ListStateMachineVersionsCommand,
7 | ListStateMachineVersionsCommandOutput,
8 | DescribeStateMachineCommand,
9 | } from '@aws-sdk/client-sfn';
10 | import { fromEnv } from '@aws-sdk/credential-providers';
11 |
12 | /**
13 | * Reads access token from these environment variables:
14 | * AWS_ACCESS_KEY_ID
15 | * AWS_SECRET_ACCESS_KEY
16 | *
17 | * Using fromEnv() to automatically pull the data in from environment variables
18 | * When I tried specifying the creditials manually, TypeScript complained.
19 | * This could probably be done without the need for
20 | * @aws-sdk/credentials-provider library with proper TypeScript configuration.
21 | *
22 | * However, that library would be useful if we offered another way to
23 | * authenticate other than with access keys, which amazon suggests to do, so it
24 | * was worth experimenting with as well.
25 | */
26 | const sfn = new SFNClient({
27 | region: process.env.AWS_REGION,
28 | credentials: fromEnv(),
29 | });
30 |
31 | // const listStateMachines = new ListStateMachinesCommand();
32 | // const response = await sfn.send(listStateMachines);
33 | // console.log("listStateMachines response", response);
34 |
35 | // just getting the details for the first state machine returned
36 | // if (response.stateMachines && response.stateMachines[0].stateMachineArn) {
37 | // getStateMachineDetails(
38 | // "arn:aws:states:us-east-1:124355667606:stateMachine:HelloWorldVersions:1"
39 | // );
40 | // getStateMachineVersions(response);
41 | // }
42 |
43 | /**
44 | * get the detailed implementations of a state machine
45 | */
46 | async function getStateMachineDetails(
47 | stateMachineArn: string
48 | ): Promise {
49 | const describeStateMachine = new DescribeStateMachineCommand({
50 | stateMachineArn,
51 | });
52 |
53 | const response = await sfn.send(describeStateMachine);
54 | console.log('getStateMachineDetails reponse', response);
55 |
56 | return undefined;
57 | }
58 |
59 | /**
60 | * Get state machine version information for each state machine found
61 | * Not invoked anymore in this file - could use some throttling later
62 | */
63 | async function getStateMachineVersions(
64 | response: ListStateMachinesCommandOutput
65 | ): Promise {
66 | const stateMachineVersions: ListStateMachineVersionsCommandOutput[] = [];
67 |
68 | if (response.stateMachines) {
69 | for (const stateMachine of response.stateMachines) {
70 | const input = {
71 | stateMachineArn: stateMachine.stateMachineArn,
72 | };
73 | const listStateMachineVerions = new ListStateMachineVersionsCommand(
74 | input
75 | );
76 | const response = await sfn.send(listStateMachineVerions);
77 | if (response.stateMachineVersions) {
78 | console.log(
79 | 'stateMachienVersionObject:',
80 | response.stateMachineVersions
81 | );
82 | }
83 | stateMachineVersions.push(response);
84 | }
85 | }
86 | console.log('listStateMachienVersions', stateMachineVersions);
87 | return undefined;
88 | }
89 |
--------------------------------------------------------------------------------
/server/utils/parseStepFunction.ts:
--------------------------------------------------------------------------------
1 | import { NewStepRow } from "../models/types";
2 |
3 | interface ASL {
4 | States: {
5 | [stateName: string]: {
6 | Type: string;
7 | };
8 | };
9 | }
10 | interface StateContent {
11 | Type: string;
12 | Branches?: Branch[];
13 | Comment?: string;
14 | ItemProcessor?: ItemProcessor;
15 | }
16 | interface States {
17 | [stateName: string]: StateContent;
18 | }
19 | interface Branch {
20 | States: {
21 | [stateName: string]: StateContent;
22 | };
23 | }
24 | interface ItemProcessor {
25 | States: {
26 | [stateName: string]: StateContent;
27 | };
28 | }
29 | /**
30 | * Parses a step function definition written in ASL and returns all steps, so
31 | * that they can be inserted into the steps database table.
32 | * @param asl The full step function definition parsed as a javascript object
33 | * @returns Promise - promise which resolves to an array of
34 | * objects, whose keys are row names in the steps database table
35 | */
36 | const parseStepFunction = async (
37 | asl: ASL,
38 | stepFunctionId: number
39 | ): Promise => {
40 | const states = asl.States;
41 | const parsedStates: NewStepRow[] = [];
42 |
43 | const recursiveParser = async (states: States): Promise => {
44 | for (const [stateName, stateContent] of Object.entries(states)) {
45 | parsedStates.push({
46 | name: stateName,
47 | step_function_id: stepFunctionId,
48 | type: stateContent.Type,
49 | comment: stateContent.Comment,
50 | });
51 | if (stateContent.Type === "Parallel") {
52 | for (const branch of stateContent.Branches) {
53 | await recursiveParser(branch.States);
54 | }
55 | } else if (stateContent.Type === "Map") {
56 | await recursiveParser(stateContent.ItemProcessor.States);
57 | }
58 | }
59 | };
60 | await recursiveParser(states);
61 |
62 | return parsedStates;
63 | };
64 |
65 | export default parseStepFunction;
66 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | // setupTests.js
2 | import { expect } from 'vitest';
3 | import matchers from '@testing-library/jest-dom/matchers';
4 |
5 | expect.extend(matchers);
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .chartBubble {
6 | height: 100px;
7 | width: 150px;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | border: 1px solid #eee;
13 | padding: 5px;
14 | border-radius: 5px;
15 | }
16 | html,
17 | body {
18 | --main-black: rgb(20, 20, 20);
19 | --main-white: rgb(235, 235, 235);
20 | --main-blue: #4e77ff;
21 | height: 100%;
22 | margin: 0;
23 | color: var(--main-white);
24 | background-color: var(--main-black);
25 | overflow-x: hidden;
26 | /* background-color: rgb(247, 247, 247); */
27 | }
28 |
29 | .react-flow .react-flow__controls {
30 | color: black;
31 | }
32 |
33 | .plot-container {
34 | border-radius: 0.5rem;
35 | }
36 |
37 | .landingPage {
38 | display: flex;
39 | height: 75vw;
40 | width: 100vw;
41 | }
42 |
43 | .navBar {
44 | display: flex;
45 | flex-direction: column;
46 | color: var(--main-white);
47 | /* background-color: #04AA6D; */
48 | border: 1px solid black;
49 | border-radius: 5px;
50 | width: fit-content;
51 | height: 500px;
52 | margin: 20px;
53 | padding: 10px;
54 | background-color: rgb(42, 42, 42);
55 | justify-content: space-between;
56 | }
57 |
58 | .about a,
59 | .contact a,
60 | .home a,
61 | .logout a {
62 | text-decoration: none;
63 | color: var(--main-white);
64 | margin: 200px 5px;
65 | }
66 |
67 | .about a:hover,
68 | .contact a:hover,
69 | .home a:hover,
70 | .logout a:hover {
71 | color: var(--main-blue);
72 | }
73 |
74 | .addCard {
75 | display: flex;
76 | width: fit-content;
77 | margin: 20px;
78 | background-color: var(--main-black);
79 | }
80 |
81 | .addCard .add {
82 | height: 100px;
83 | width: 100px;
84 | margin: 0px;
85 | padding: 0%;
86 | background-color: var(--main-blue);
87 | opacity: 0.9;
88 | border-radius: 5px;
89 | }
90 |
91 | .add:hover {
92 | opacity: 1;
93 | }
94 |
95 | .functionCards {
96 | border: 1px solid black;
97 | margin: 20px;
98 | border-radius: 5px;
99 | }
100 |
101 | .stepFunc {
102 | position: fixed;
103 | z-index: 1000;
104 | left: 0;
105 | top: 0;
106 | width: 100%;
107 | height: 100%;
108 | display: flex;
109 | justify-content: center;
110 | align-items: center;
111 | background-color: rgba(0, 0, 0, 0.1);
112 | }
113 |
114 | .stepFunc form {
115 | display: flex;
116 | flex-direction: column;
117 | align-items: center;
118 | border: 1px solid black;
119 | border-radius: 5px;
120 | background-color: white;
121 | }
122 |
123 | .stepFunc form label,
124 | .stepFunc form input,
125 | .stepFunc form button {
126 | margin: 5px 20px;
127 | }
128 |
129 | .stepFunc form label,
130 | .stepFunc form button {
131 | border-radius: 5px;
132 | }
133 |
134 | .stepFunc form input,
135 | .stepFunc form button {
136 | border: 1px solid black;
137 | border-radius: 5px;
138 |
139 | }
140 | /* slider */
141 | .slidecontainer {
142 | width: 300px;
143 | margin-left: auto;
144 | margin-right: auto;
145 | }
146 |
147 | .slider {
148 | -webkit-appearance: none;
149 | width: 100%;
150 | height: 10px;
151 | background: #d3d3d3;
152 | outline: none;
153 | opacity: 0.7;
154 | -webkit-transition: 0.2s;
155 | transition: opacity 0.2s;
156 | border-radius: 25px;
157 | }
158 |
159 | .slider:hover {
160 | opacity: 1;
161 | }
162 |
163 | .slider::-webkit-slider-thumb {
164 | -webkit-appearance: none;
165 | appearance: none;
166 | width: 10px;
167 | height: 25px;
168 | background: #4e77ff;
169 | cursor: pointer;
170 | border-radius: 50%;
171 | }
172 |
173 | .slider::-moz-range-thumb {
174 | width: 25px;
175 | height: 25px;
176 | background: #04aa6d;
177 | cursor: pointer;
178 | }
179 |
180 | /* .rightSideLineGraph {
181 | height: 500px;
182 | width: 500px;
183 | } */
184 | #graph-style {
185 | /* margin-left: auto; */
186 | /* margin-right: auto; */
187 | border: solid black;
188 | }
189 |
190 | .dv-btn {
191 | display: block;
192 | background-color: var(--main-blue);
193 | opacity: 0.9;
194 | height: 30px;
195 | width: 60px;
196 | margin: 5px;
197 | border-radius: 5px;
198 | float: right;
199 | position: relative;
200 | box-shadow: 3px 3px 0px 0px black;
201 | }
202 | .detailedView {
203 | display: flex;
204 | }
205 |
206 | .dv-btn:hover {
207 | opacity: 1;
208 | /* width: 100px;
209 | height: 50px; */
210 | }
211 | /* #detailed-view{
212 | height: auto;
213 | background-color: blue;
214 | } */
215 |
216 | .popupLineGraph {
217 | position: fixed;
218 | top: 50%;
219 | left: 50%;
220 | transform: translate(-50%, -50%);
221 | background-color: white;
222 | border-radius: 5px;
223 | padding: 16px;
224 | z-index: 1000;
225 | display: flex;
226 | flex-direction: column;
227 | align-items: center;
228 | width: 80vw;
229 | max-width: 800px;
230 | height: 55vh;
231 | max-height: 600px;
232 | }
233 |
234 | .popupOverlay {
235 | position: fixed;
236 | top: 0;
237 | left: 0;
238 | width: 100%;
239 | height: 100%;
240 | background-color: rgba(0, 0, 0, 0.7);
241 | z-index: 999;
242 | }
243 |
244 | .popupLineGraph button {
245 | border-radius: 5px;
246 | width: 25%;
247 | }
248 |
249 |
250 | .heatmapChart {
251 | border-radius: 5px;
252 | overflow: hidden;
253 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
254 | }
255 |
256 | .detailedView {
257 | /* margin-right: 200px; Adjust as needed */
258 | margin-left: 0%;
259 | }
260 |
261 | .my-10 {
262 | width: 100%; /* or a fixed width, e.g., 600px */
263 | height: 350px; /* Set a fixed height to maintain aspect ratio */
264 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | // import { useState } from 'react';
2 | import './App.css';
3 | // import DetailedView from './DetailedView/DetailedView';
4 | // import LandingPage from './landingPage/landingPage';npm
5 | // import LoginForm from './loginForm';
6 | // import SignUpForm from './signUpForm';
7 | import { Provider } from 'react-redux';
8 | import DetailedView from './DetailedView/DetailedView.tsx';
9 | import store /*persistor*/ from '../store.tsx';
10 | import LandingPage from './landingPage/landingPage.tsx';
11 | import NavBar from './landingPage/navbar/navBar.tsx';
12 | import Dashboard from './landingPage/Dashboard.tsx';
13 | import DetailedViewContainer from './DetailedView/DetailedViewContainer.tsx';
14 |
15 | import { Route, Routes, Link } from 'react-router-dom';
16 | // import { PersistGate } from 'redux-persist/integration/react';
17 |
18 | function App() {
19 | // return (
20 | // <>
21 | //
22 | // {/* */}
23 | // {/* */}
24 | // {/* */}
25 | //
26 | //
27 | // >
28 | // );old
29 |
30 | return (
31 | // <>
32 | //
33 | // {/* */}
34 | // {/* */}
35 | // {/* */}
36 | //
37 | //
38 | // >
39 | // );
40 |
41 | {/* */}
42 |
43 | {/* }> */}
44 | } />
45 | }>
46 | {/*possible to nest comps in here dont know that I will */}
47 | {/* */}
48 |
49 |
50 | {/* */}
51 |
52 | // >
53 | );
54 | }
55 |
56 | export default App;
57 |
58 | // app
59 |
60 | // login/sign
61 | // login
62 | // sign up
63 |
64 | // landing
65 | // nav bar
66 | // Diff options (home, about us, etc)
67 | // step function cards
68 | // add function button
69 |
70 | // detailed view
71 | // UI
72 | // Back button
73 | // Flow Chart
74 | // flowchart view
75 | // Flowchart elements
76 | // Ability to click into and see graph
77 | // Filter for data to show
78 | // Select function button
79 | // Data Container
80 | // Collapsable menu of diff visualization tools
81 | // Data visualization tool component
82 | // Toggle time period button/filter
83 | // Time Slice
84 | // Slider with time range
85 | // Toggle between day/week/month button
86 |
--------------------------------------------------------------------------------
/src/DetailedView/BackButton.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 | import { setLatency, setLatencies, setBubbleName, setTimeToggle } from '../reducers/dataSlice';
3 | import { AppDispatch } from "../../store.tsx";
4 | import { useDispatch } from 'react-redux';
5 |
6 |
7 | function BackButton() {
8 | const navigate = useNavigate()
9 | const dispatch:AppDispatch = useDispatch();
10 |
11 | function goBack(){
12 | dispatch(setLatency([]))
13 | dispatch(setLatencies([]))
14 | dispatch(setBubbleName(''))
15 | dispatch(setTimeToggle('hours'))
16 | navigate('/');
17 | }
18 | return ;
19 | }
20 |
21 | export default BackButton;
22 |
--------------------------------------------------------------------------------
/src/DetailedView/DataContainer.tsx:
--------------------------------------------------------------------------------
1 | import TimePeriodToggle from './TimePeriodToggle';
2 | import DataVisualization from './DataVisualization';
3 | import StepDataVisualization from './StepDataVisualization';
4 | import HeatmapChart from './HeatmapChart';
5 | import { useSelector } from 'react-redux';
6 | import { selectData } from '../reducers/dataSlice';
7 |
8 | function DataContainer() {
9 | const data = useSelector(selectData)
10 | return (
11 |
12 | {/* This is the Data Container */}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {data.bubblePopup && (
21 |
22 |
23 |
24 | )}
25 |
26 |
27 | );
28 | }
29 |
30 | export default DataContainer;
31 |
--------------------------------------------------------------------------------
/src/DetailedView/DataVisualization.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 | import { Chart, registerables } from 'chart.js';
3 | import { useSelector } from 'react-redux';
4 | import moment from 'moment';
5 | import { RootState } from '../../store';
6 | // import { selectData } from '../reducers/dataSlice';
7 |
8 | function DataVisualization() {
9 | const canvasRef = useRef(null);
10 | let chartInstance = useRef(null);
11 | const latency = useSelector((state: RootState) => state.data.latencies);
12 | const timePeriod = useSelector((state: RootState) => state.data.time);
13 | // console.log('p',latency)
14 |
15 | const latencies = latency.map((item) =>
16 | Object.keys(item).length === 0 ? null : item.stepFunctionAverageLatency
17 | );
18 | console.log('Function latencies: ', latencies);
19 |
20 | Chart.register(...registerables);
21 |
22 | // function generateLast24Hours() {
23 | // return Array.from({ length: 24 }, (_, i) =>
24 | // moment()
25 | // .subtract(23 - i, 'hours')
26 | // .format('HH:mm')
27 | // );
28 | // }
29 | function generateTimes() {
30 | return Array.from({ length: 24 }, (_, i) =>
31 | `${String(i).padStart(2, '0')}:00`
32 | );
33 | }
34 | function generateLast7Days() {
35 | return Array.from({ length: 7 }, (_, i) =>
36 | moment()
37 | .subtract(6 - i, 'days')
38 | .format('MM/DD')
39 | );
40 | }
41 |
42 | function generateLast12Weeks() {
43 | return Array.from({ length: 12 }, (_, i) =>
44 | moment()
45 | .subtract(11 - i, 'weeks')
46 | .format('MM/DD')
47 | );
48 | }
49 |
50 | function generateLast12Months() {
51 | return Array.from({ length: 12 }, (_, i) =>
52 | moment()
53 | .subtract(11 - i, 'months')
54 | .format('MM/YYYY')
55 | );
56 | }
57 |
58 | useEffect(() => {
59 | if (chartInstance.current) {
60 | chartInstance.current.destroy();
61 | }
62 |
63 | // let xValues = latency.map((set) => moment(set.date).format('HH:mm'))
64 | let xValues = generateTimes();
65 | let timeLabel = '24 Hours';
66 |
67 | if (latency) {
68 | if (timePeriod == 'days') {
69 | xValues = generateLast7Days();
70 | timeLabel = '7 Days';
71 | } else if (timePeriod == 'weeks') {
72 | xValues = generateLast12Weeks();
73 | timeLabel = '12 (Full) Weeks';
74 | } else if (timePeriod == 'months') {
75 | xValues = generateLast12Months();
76 | timeLabel = '12 Months';
77 | }
78 | }
79 |
80 | chartInstance.current = new Chart(canvasRef.current, {
81 | type: 'line',
82 | data: {
83 | labels: xValues,
84 | datasets: [
85 | {
86 | label: `Latency Overview Across ${timeLabel}`,
87 | data: latencies,
88 | fill: false,
89 | },
90 | ],
91 | },
92 | options: {
93 | responsive:true,
94 | spanGaps: false,
95 | },
96 | });
97 |
98 | // return () => {
99 | // chartInstance.current.destroy();
100 | // };
101 | }, [latency]);
102 |
103 | // function handleClose() {
104 |
105 | // }
106 |
107 | return (
108 |
109 |
110 | {/* */}
111 |
112 | );
113 | }
114 |
115 | export default DataVisualization;
116 |
--------------------------------------------------------------------------------
/src/DetailedView/DetailedView.tsx:
--------------------------------------------------------------------------------
1 |
2 | import FlowChart from './FlowChart';
3 | import DataContainer from './DataContainer';
4 | import TimeSlice from './TimeSlice';
5 | import { useEffect } from 'react';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { AppDispatch, RootState } from '../../store';
8 | import { getLatencies, setLatencies } from '../reducers/dataSlice';
9 |
10 | function DetailedView() {
11 | //const [function, setFunction] = useState({});
12 | const dispatch: AppDispatch = useDispatch();
13 | // function onclick() {
14 | // fetch('/api/step-functions/addStepFunction', {
15 | // method: 'POST',
16 | // headers: { 'Content-Type': 'application/json' },
17 | // body: JSON.stringify({
18 | // arn: 'arn:aws:states:us-west-2:703671926773:stateMachine:BasicsHelloWorldStateMachine',
19 | // }),
20 | // }).then((data) => {
21 | // return data.json();
22 | // });
23 | // // .then((data) => console.log(data));
24 | // }
25 | // function getall() {
26 | // fetch('api/step-functions').then((data) => {
27 | // return data.json();
28 | // });
29 | // // .then((data) => console.log(data));
30 | // }
31 | const definitionID = useSelector(
32 | (state: RootState) => state.data.currentDefinitionID
33 | );
34 |
35 |
36 | const timeToggle = useSelector((state: RootState) => state.data.time)
37 |
38 |
39 | // useEffect(() => {
40 | // dispatch(getLatencies(definitionID, timeToggle))
41 | // .unwrap()
42 | // // .then((data) => {
43 | // // console.log('d',data);
44 | // // return data;
45 | // // })
46 | // .then((data) => dispatch(setLatencies(data)));
47 | // }, [dispatch, definitionID, timeToggle]);
48 |
49 | useEffect(() => {
50 | if (definitionID && timeToggle) {
51 | dispatch(getLatencies({ id: definitionID, time: timeToggle }))
52 | .unwrap()
53 | .then((data) => {
54 | dispatch(setLatencies(data))
55 | });
56 | }
57 | }, [dispatch, definitionID, timeToggle]);
58 |
59 | // useEffect(() => {
60 | // const fetchData = async () => {
61 | // const data = await dispatch(getLatencies({ id: definitionID, time: timeToggle }));
62 | // dispatch(setLatencies(data));
63 | // };
64 | // fetchData();
65 | // }, [definitionID, timeToggle, dispatch]);
66 |
67 | return (
68 |
69 | {/* This is the detailed view */}
70 | {/*
71 | */}
72 |
73 |
74 | {/* this is just the back button */}
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | export default DetailedView;
82 |
--------------------------------------------------------------------------------
/src/DetailedView/DetailedViewContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Disclosure, DisclosureButton, DisclosurePanel, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
2 | import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline'
3 | import DetailedView from './DetailedView.tsx';
4 | import {useLocation} from 'react-router-dom'
5 | import DetailedViewUI from './DetailedViewUI';
6 |
7 | const user = {
8 | name: 'Tom Cook',
9 | email: 'tom@example.com',
10 | imageUrl:
11 | 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
12 | }
13 | const navigation = [
14 | { name: 'Dashboard', href: '#', current: true },
15 | { name: 'Team', href: '#', current: false },
16 | { name: 'Projects', href: '#', current: false },
17 | { name: 'Calendar', href: '#', current: false },
18 | { name: 'Reports', href: '#', current: false },
19 | ]
20 | const userNavigation = [
21 | { name: 'Your Profile', href: '#' },
22 | { name: 'Settings', href: '#' },
23 | { name: 'Sign out', href: '#' },
24 | ]
25 |
26 | function classNames(...classes) {
27 | return classes.filter(Boolean).join(' ')
28 | }
29 |
30 | export default function DetailedViewContainer() {
31 | const location = useLocation();
32 | const { cardName } = location.state
33 | return (
34 | <>
35 | {/*
36 | This example requires updating your template:
37 |
38 | ```
39 |
40 |
41 | ```
42 | */}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {navigation.map((item) => (
52 |
62 | {item.name}
63 |
64 | ))}
65 |
66 |
67 |
68 |
69 |

70 |
71 |
72 |
{user.name}
73 |
{user.email}
74 |
75 |
83 |
84 |
85 | {userNavigation.map((item) => (
86 |
92 | {item.name}
93 |
94 | ))}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
{cardName}
104 |
105 |
106 |
107 |
108 |
109 |
110 | >
111 | )
112 | }
113 |
--------------------------------------------------------------------------------
/src/DetailedView/DetailedViewUI.tsx:
--------------------------------------------------------------------------------
1 | import BackButton from './BackButton';
2 |
3 | function DetailedViewUI() {
4 | return (
5 |
6 | {/* this is the detailed view UI */}
7 |
8 |
9 | );
10 | }
11 |
12 | export default DetailedViewUI;
13 |
--------------------------------------------------------------------------------
/src/DetailedView/FlowChart.tsx:
--------------------------------------------------------------------------------
1 | //import React from 'react';
2 | import FlowChartView from './FlowChartView';
3 | import FlowChartDataSelector from './FlowChartDataSelector';
4 | import StepFunctionSelector from './StepFunctionSelector';
5 | import { useSelector } from 'react-redux';
6 | import { RootState } from '../../store';
7 | import TimeSlice from './TimeSlice';
8 |
9 | function FlowChart() {
10 | const definition = useSelector(
11 | (state: RootState) => state.data.currentDefinition
12 | );
13 | return (
14 |
15 | {/* This is the flow chart */}
16 |
17 | {/* */}
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default FlowChart;
25 |
--------------------------------------------------------------------------------
/src/DetailedView/FlowChartBubble.tsx:
--------------------------------------------------------------------------------
1 | import { Handle, Position } from '@xyflow/react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { RootState, AppDispatch } from '../../store';
4 | import {
5 | setChartLatencies,
6 | setBubblePopup,
7 | setBubbleName,
8 | } from '../reducers/dataSlice';
9 |
10 | type BubbleProps = {
11 | data: {
12 | metric: number;
13 | name: string;
14 | latency: number[];
15 | };
16 | };
17 |
18 | function FlowChartBubble({ data }: BubbleProps) {
19 | const dispatch: AppDispatch = useDispatch();
20 |
21 | const getColor = (num: number, max: number = 15): string => {
22 | let red: number;
23 | let green: number;
24 | let halfRatio: number;
25 |
26 | if (num <= max / 2) {
27 | halfRatio = num / (max / 2);
28 | red = Math.floor(255 * halfRatio);
29 | green = 255;
30 | } else {
31 | halfRatio = ((num - max / 2) / max) * 2;
32 | red = 255;
33 | green = Math.floor(255 - 255 * halfRatio);
34 | }
35 | return `rgb(${red}, ${green}, 0)`;
36 | };
37 |
38 | function handleClick(e: React.MouseEvent) {
39 | e.preventDefault();
40 | dispatch(setChartLatencies(data.name));
41 | dispatch(setBubblePopup(true));
42 | dispatch(setBubbleName(data.name));
43 | }
44 |
45 | const latency = useSelector((state: RootState) => state.data.latency);
46 | let average = 0;
47 | let color = 'gray';
48 |
49 | if (latency && latency.hasOwnProperty('steps')) {
50 | average = latency.steps[data.name]?.average || 0;
51 | color = getColor(average);
52 | }
53 |
54 | return (
55 |
66 | );
67 | }
68 |
69 | export default FlowChartBubble;
70 |
--------------------------------------------------------------------------------
/src/DetailedView/FlowChartDataSelector.tsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from "react-router-dom";
2 |
3 |
4 | function FlowChartDataSelector() {
5 | const location = useLocation();
6 | const { cardName } = location.state
7 | return (
8 |
9 | {cardName}
10 | {/* This is the flow chart data selector */}
11 |
12 | )
13 | }
14 |
15 | export default FlowChartDataSelector;
16 |
--------------------------------------------------------------------------------
/src/DetailedView/FlowChartView.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ReactFlow,
3 | MiniMap,
4 | Controls,
5 | Background,
6 | BackgroundVariant,
7 | NodeTypes,
8 | BuiltInNode,
9 | ReactFlowProvider,
10 | } from '@xyflow/react';
11 | import '@xyflow/react/dist/style.css';
12 | import { useEffect, useMemo, useState } from 'react';
13 | import FlowChartBubble from './FlowChartBubble';
14 | import dagre, { Node } from '@dagrejs/dagre';
15 | import { useSelector } from 'react-redux';
16 | import { RootState } from '../../store.tsx';
17 |
18 | type FlowChartNode = {
19 | id: string;
20 | type: string;
21 | position: { x: number; y: number };
22 | data: { metric: number; name: string | undefined };
23 | };
24 | //{ id: 'e1-2', source: '1', target: '2' }
25 | type FlowChartEdge = {
26 | id: string;
27 | source: string;
28 | target: string;
29 | };
30 |
31 | type NodesAndEdges = {
32 | nodes: FlowChartNode[];
33 | edges: FlowChartEdge[];
34 | };
35 |
36 | function FlowChartView({ size, definition }) {
37 | const nodeTypes = useMemo(() => ({ flowChartBubble: FlowChartBubble }), []);
38 | // const [initialNodes, setInitialNodes] = useState([]);
39 | // const [initialEdges, setInitialEdges] = useState([]);
40 |
41 | const g = new dagre.graphlib.Graph();
42 |
43 | g.setGraph({});
44 |
45 | g.setDefaultEdgeLabel(function () {
46 | return {};
47 | });
48 |
49 | function createFlowchart(g, data) {
50 | if (!data) return { nodes: [], edges: [] };
51 | function createGraph(g, subgraph, next?) {
52 | for (const state in subgraph.States) {
53 | g.setNode(state, { label: state, width: 200, height: 200 });
54 | if (
55 | subgraph.States[state].Next &&
56 | !(subgraph.States[state].Type === 'Parallel')
57 | )
58 | g.setEdge(state, subgraph.States[state].Next);
59 |
60 | if (subgraph.States[state].Catch) {
61 | subgraph.States[state].Catch.forEach((ele) => {
62 | g.setEdge(state, ele.Next);
63 | });
64 | }
65 |
66 | if (subgraph.States[state].Type === 'Choice') {
67 | subgraph.States[state].Choices.forEach((ele) => {
68 | g.setEdge(state, ele.Next);
69 | });
70 | }
71 | if (subgraph.States[state].Type === 'Parallel') {
72 | subgraph.States[state].Branches.forEach((ele) => {
73 | createGraph(g, ele, subgraph.States[state].Next);
74 | g.setEdge(state, ele.StartAt);
75 | });
76 | }
77 | if (subgraph.States[state].End && next) {
78 | g.setEdge(state, next);
79 | }
80 | }
81 | }
82 | createGraph(g, data);
83 |
84 | dagre.layout(g);
85 |
86 | const initialNodes = [];
87 | const initialEdges = [];
88 |
89 | g.nodes().forEach(function (v) {
90 | let data = 0;
91 | const newNode = {
92 | id: g.node(v).label,
93 | type: 'flowChartBubble',
94 | position: { x: g.node(v).x, y: g.node(v).y },
95 | data: {
96 | metric: data, //latency, //Math.floor(Math.random() * 255),
97 | name: g.node(v).label,
98 | },
99 | };
100 | initialNodes.push(newNode);
101 | });
102 |
103 | g.edges().forEach(function (e) {
104 | const newEdge = {
105 | id: `${e.v}->${e.w}`,
106 | source: e.v,
107 | target: e.w,
108 | type: 'simplebezier',
109 | style: { stroke: 'rgb(50, 50, 50)', strokeWidth: 5 },
110 | };
111 | initialEdges.push(newEdge);
112 | });
113 | return { nodes: initialNodes, edges: initialEdges };
114 | }
115 |
116 | const results = createFlowchart(g, definition);
117 | const initialNodes = results.nodes;
118 | const initialEdges = results.edges;
119 |
120 | return (
121 |
122 |
123 | instance.fitView()}
128 | >
129 |
130 |
131 |
132 |
133 |
134 | );
135 | }
136 |
137 | export default FlowChartView;
138 |
--------------------------------------------------------------------------------
/src/DetailedView/HeatmapChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 | import Plotly from 'plotly.js-dist';
3 | import { useSelector } from 'react-redux';
4 | import moment from 'moment';
5 | import { RootState } from '../../store';
6 |
7 | type Annotation = {
8 | xref: string;
9 | yref: string;
10 | x: string;
11 | y: string;
12 | text: number;
13 | font: {
14 | family: string;
15 | size: number;
16 | color: string;
17 | };
18 | showarrow: boolean;
19 | };
20 |
21 | type xaxis = {
22 | ticks: string;
23 | side: string;
24 | zeroline: boolean;
25 | title: string;
26 | automargin: boolean;
27 | tickangle: number;
28 | };
29 |
30 | type yaxis = {
31 | ticks: string;
32 | ticksuffix: string;
33 | autosize: boolean;
34 | automargin: boolean;
35 | title: string;
36 | };
37 |
38 | function HeatmapChart() {
39 | const plotRef = useRef(null);
40 | const dataset = useSelector((state: RootState) => state.data.latencies);
41 | console.log('dat',dataset)
42 | const timePeriod = useSelector((state: RootState) => state.data.time);
43 |
44 | const placeholderData = [
45 | {
46 | date: new Date(),
47 | steps: { step1: { average: 0 }, step2: { average: 0 } },
48 | },
49 | ];
50 |
51 | const currentData = dataset && dataset.length > 0 ? dataset : placeholderData;
52 |
53 | // function generateLast24Hours() {
54 | // return Array.from({ length: 24 }, (_, i) =>
55 | // moment()
56 | // .subtract(23 - i, 'hours')
57 | // .format('HH:mm')
58 | // );
59 | // }
60 | function generateTimes() {
61 | return Array.from({ length: 24 }, (_, i) =>
62 | `${String(i).padStart(2, '0')}:00`
63 | );
64 | }
65 |
66 | function generateLast7Days() {
67 | return Array.from({ length: 7 }, (_, i) =>
68 | moment()
69 | .subtract(6 - i, 'days')
70 | .format('MM/DD')
71 | );
72 | }
73 | function generateLast12Weeks() {
74 | return Array.from({ length: 12 }, (_, i) =>
75 | moment()
76 | .subtract(11 - i, 'weeks')
77 | .format('MM/DD')
78 | );
79 | }
80 |
81 | function generateLast12Months() {
82 | return Array.from({ length: 12 }, (_, i) =>
83 | moment()
84 | .subtract(11 - i, 'months')
85 | .format('MM/YYYY')
86 | );
87 | }
88 |
89 | useEffect(() => {
90 | if (!currentData || currentData.length === 0) return;
91 | let xValues;
92 | // let xValues = currentData.map((set) => moment(set.date).format('HH:mm'));
93 | if (timePeriod === 'hours') {
94 | // xValues = generateLast24Hours();
95 | xValues = generateTimes();
96 |
97 | }
98 | if (timePeriod === 'days') {
99 | xValues = generateLast7Days();
100 | } else if (timePeriod === 'weeks') {
101 | xValues = generateLast12Weeks();
102 | } else if (timePeriod === 'months') {
103 | xValues = generateLast12Months();
104 | }
105 |
106 | const addLineBreaks = (label: string, maxLength: number) => {
107 | const words = label.split(' ');
108 | let line = '';
109 | const lines = [];
110 |
111 | words.forEach((word) => {
112 | if ((line + word).length > maxLength) {
113 | lines.push(line);
114 | line = '';
115 | }
116 | line += word + ' ';
117 | });
118 | lines.push(line.trim());
119 |
120 | return { original: label, processed: lines.join('
') };
121 | };
122 | const validData = currentData.find((set) => set.steps);
123 | const yValues = Object.keys(validData.steps).map((label) =>
124 | addLineBreaks(label, 15)
125 | );
126 | const zValues = currentData.map((set) =>
127 | yValues.map((step) => {
128 | /*set.steps[step.original]?.average || 0*/
129 | if (set.steps && set.steps[step.original]) {
130 | return set.steps[step.original].average;
131 | }
132 | return null;
133 | })
134 | );
135 |
136 | const processedYValues = yValues.map((step) => step.processed);
137 | const transposedZValues = zValues[0].map((_, colIndex) =>
138 | zValues.map((row) => row[colIndex])
139 | );
140 |
141 | const data = [
142 | {
143 | x: xValues,
144 | y: processedYValues,
145 | z: transposedZValues,
146 | type: 'heatmap',
147 | colorscale: [
148 | [0, '#78fa4c'],
149 | [1, '#ff4136'],
150 | ],
151 | showscale: true,
152 | },
153 | ];
154 |
155 | const layout: {
156 | annotations: Annotation[];
157 | title: string;
158 | width: number;
159 | xaxis: xaxis;
160 | yaxis: yaxis;
161 | } = {
162 | title: 'Heatmap Overview',
163 | annotations: [],
164 | xaxis: {
165 | ticks: '',
166 | side: 'bottom',
167 | zeroline: false,
168 | title: 'Time',
169 | automargin: true,
170 | tickangle: 315,
171 | },
172 | yaxis: {
173 | ticks: '',
174 | ticksuffix: ' ',
175 | autosize: false,
176 | automargin: true,
177 | title: 'Step Function Action',
178 | },
179 | width: '100%',
180 | // plot_bgcolor: 'black',
181 | paper_bgcolor: 'rgb(172,104,197)',
182 | // 'opacity-80 bg-gradient-to-br from-purple-600 to-fuchsia-400 rounded-3xl mx-10',
183 | // plot_bgcolor: 'rgba(0,0,0,0)',
184 | // paper_bgcolor: 'rgba(0,0,0,0)',
185 | };
186 |
187 | if (plotRef.current) {
188 | Plotly.newPlot(plotRef.current, data, layout);
189 | }
190 |
191 | return () => {
192 | if (plotRef.current) {
193 | Plotly.purge(plotRef.current);
194 | }
195 | };
196 | }, [currentData, timePeriod]);
197 |
198 | return (
199 |
202 | );
203 | }
204 |
205 | export default HeatmapChart;
206 |
--------------------------------------------------------------------------------
/src/DetailedView/StepDataVisualization.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 | import { Chart, registerables } from 'chart.js';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import moment from 'moment';
5 | import { RootState } from '../../store';
6 | import { setBubblePopup } from '../reducers/dataSlice';
7 | import { Background } from '@xyflow/react';
8 | // import { selectData } from '../reducers/dataSlice';
9 |
10 | export default function StepDataVisualization() {
11 | const canvasRef = useRef(null);
12 | const chartInstance = useRef(null);
13 | const latency = useSelector((state: RootState) => state.data.chartLatencies);
14 | const times = useSelector((state: RootState) => state.data.latencies);
15 | const timePeriod = useSelector((state: RootState) => state.data.time);
16 | const bubbleName = useSelector((state: RootState) => state.data.bubbleName);
17 | console.log('Action latencies: ', latency);
18 |
19 | const dispatch = useDispatch();
20 |
21 | // console.log('l',latency)
22 |
23 | // const latencies = latency.map((item) => item.stepFunctionAverageLatency);
24 | // console.log('hi',latencies)
25 |
26 | Chart.register(...registerables);
27 |
28 | // function generateLast24Hours() {
29 | // return Array.from({ length: 24 }, (_, i) =>
30 | // moment()
31 | // .subtract(23 - i, 'hours')
32 | // .format('HH:mm')
33 | // );
34 | // }
35 |
36 | function generateTimes() {
37 | return Array.from({ length: 24 }, (_, i) =>
38 | `${String(i).padStart(2, '0')}:00`
39 | );
40 | }
41 |
42 | function generateLast7Days() {
43 | return Array.from({ length: 7 }, (_, i) =>
44 | moment()
45 | .subtract(6 - i, 'days')
46 | .format('MM/DD')
47 | );
48 | }
49 |
50 | function generateLast12Weeks() {
51 | return Array.from({ length: 12 }, (_, i) =>
52 | moment()
53 | .subtract(11 - i, 'weeks')
54 | .format('MM/DD')
55 | );
56 | }
57 |
58 | function generateLast12Months() {
59 | return Array.from({ length: 12 }, (_, i) =>
60 | moment()
61 | .subtract(11 - i, 'months')
62 | .format('MM/YYYY')
63 | );
64 | }
65 |
66 | useEffect(() => {
67 | if (chartInstance.current) {
68 | chartInstance.current.destroy();
69 | }
70 |
71 | let xValues = generateTimes();
72 | let timeLabel = '24 Hours';
73 |
74 | if (times) {
75 | if (timePeriod == 'days') {
76 | xValues = generateLast7Days();
77 | timeLabel = '7 Days';
78 | } else if (timePeriod == 'weeks') {
79 | xValues = generateLast12Weeks();
80 | timeLabel = '12 Weeks';
81 | } else if (timePeriod == 'months') {
82 | xValues = generateLast12Months();
83 | timeLabel = '12 Months';
84 | }
85 | }
86 |
87 | chartInstance.current = new Chart(canvasRef.current, {
88 | type: 'line',
89 | data: {
90 | labels: xValues,
91 | datasets: [
92 | {
93 | label: `"${bubbleName}" Latency Across ${timeLabel}`,
94 | data: latency,
95 | },
96 | ],
97 | },
98 | });
99 |
100 | // return () => {
101 | // chartInstance.current.destroy();
102 | // };
103 | }, [latency, timePeriod, times]);
104 |
105 | // function handleClose() {
106 |
107 | // }
108 |
109 | const handleClick = () => {
110 | dispatch(setBubblePopup(false));
111 | };
112 |
113 | return (
114 |
115 |
116 |
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/DetailedView/StepFunctionSelector.tsx:
--------------------------------------------------------------------------------
1 | function StepFunctionSelector() {
2 | return {/*This is the StepFunctionSelector*/}
;
3 | }
4 |
5 | export default StepFunctionSelector;
6 |
--------------------------------------------------------------------------------
/src/DetailedView/TimePeriodToggle.tsx:
--------------------------------------------------------------------------------
1 | function TimePeriodToggle() {
2 | return {/*This is the time period toggle*/}
;
3 | }
4 |
5 | export default TimePeriodToggle;
6 |
--------------------------------------------------------------------------------
/src/DetailedView/TimeSlice.tsx:
--------------------------------------------------------------------------------
1 | import TimeSlider from './TimeSlider';
2 | import TimeToggle from './TimeToggle';
3 |
4 | function TimeSlice() {
5 | return (
6 |
7 | {/* This is the time slice */}
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default TimeSlice;
15 |
--------------------------------------------------------------------------------
/src/DetailedView/TimeSlider.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { setLatency } from '../reducers/dataSlice';
4 | import { RootState } from '../../store';
5 | import selectData, { setTimeToggle } from '../reducers/dataSlice.tsx';
6 |
7 | const getColor = (num: number, max: number = 100): string => {
8 | //the score form green to red is between 1 and max
9 | //colors in rgb
10 | let red: number;
11 | let green: number;
12 | let halfRatio: number;
13 | //red starts at zero and increases to the half way point remains at 255
14 | //green starts a 255, starts decreaseing at the half way point
15 | //const latencies = useSelector((state: RootState) => state.data.latencies);
16 | if (num <= max / 2) {
17 | halfRatio = num / (max / 2);
18 |
19 | red = Math.floor(255 * halfRatio);
20 | green = 255;
21 | } else {
22 | halfRatio = ((num - max / 2) / max) * 2;
23 |
24 | red = 255;
25 | green = Math.floor(255 - 255 * halfRatio);
26 | }
27 | return `rgb(${red}, ${green}, 0)`;
28 | };
29 |
30 | function TimeSlider() {
31 | // Define state to hold the value of the slider
32 | const [sliderValue, setSliderValue] = useState(50);
33 | const [max, setMax] = useState(23);
34 | const dispatch = useDispatch();
35 | const data = useSelector((state: RootState) => state.data);
36 |
37 | useEffect(() => {
38 | switch (data.time) {
39 | case 'hours':
40 | setMax(23);
41 | break;
42 | case 'days':
43 | setMax(6);
44 | break;
45 | case 'weeks':
46 | setMax(11);
47 | break;
48 | case 'months':
49 | setMax(11);
50 | break;
51 | default:
52 | setMax(23);
53 | }
54 | }, [data.time]);
55 |
56 | // Update the state when the slider value changes
57 | const handleSliderChange = (event) => {
58 | setSliderValue(event.target.value);
59 | dispatch(setLatency(event.target.value));
60 | //dispatch(setLatency(getColor(event.target.value, 100)));
61 | };
62 |
63 | return (
64 |
65 |
73 | {/*
Value: {sliderValue}
*/}
74 |
75 | );
76 | }
77 |
78 | export default TimeSlider;
79 |
--------------------------------------------------------------------------------
/src/DetailedView/TimeToggle.tsx:
--------------------------------------------------------------------------------
1 | import { setTimeToggle } from '../reducers/dataSlice.tsx';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { AppDispatch, RootState } from '../../store.tsx';
4 |
5 | function TimeToggle() {
6 | const dispatch: AppDispatch = useDispatch();
7 | const data = useSelector((state: RootState) => state.data);
8 |
9 | const handleChange = (e: React.ChangeEvent) => {
10 | dispatch(setTimeToggle(e.target.value));
11 | };
12 |
13 | return (
14 |
15 |
18 |
24 |
25 | );
26 | }
27 |
28 | export default TimeToggle;
29 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/TimeClimb/e54c4665b69a69ffb8ccda6af0005d4a001f7a6c/src/index.css
--------------------------------------------------------------------------------
/src/landingPage/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { Disclosure, DisclosureButton, DisclosurePanel, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
2 | import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline'
3 | import AllCards from './allCards'
4 | import LandingPage from './landingPage'
5 |
6 | const user = {
7 | name: 'Tom Cook',
8 | email: 'tom@example.com',
9 | imageUrl:
10 | 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
11 | }
12 | const navigation = [
13 | { name: 'Dashboard', href: '#', current: true },
14 | { name: 'Team', href: '#', current: false },
15 | { name: 'Projects', href: '#', current: false },
16 | { name: 'Calendar', href: '#', current: false },
17 | { name: 'Reports', href: '#', current: false },
18 | ]
19 | const userNavigation = [
20 | { name: 'Your Profile', href: '#' },
21 | { name: 'Settings', href: '#' },
22 | { name: 'Sign out', href: '#' },
23 | ]
24 |
25 | function classNames(...classes) {
26 | return classes.filter(Boolean).join(' ')
27 | }
28 |
29 | export default function Dashboard() {
30 | return (
31 | <>
32 | {/*
33 | This example requires updating your template:
34 |
35 | ```
36 |
37 |
38 | ```
39 | */}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {navigation.map((item) => (
49 |
59 | {item.name}
60 |
61 | ))}
62 |
63 |
64 |
65 |
66 |

67 |
68 |
69 |
{user.name}
70 |
{user.email}
71 |
72 |
80 |
81 |
82 | {userNavigation.map((item) => (
83 |
89 | {item.name}
90 |
91 | ))}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
Time Climb
100 |
101 |
102 |
103 |
104 |
105 |
106 | >
107 | )
108 | }
109 |
--------------------------------------------------------------------------------
/src/landingPage/addCard.tsx:
--------------------------------------------------------------------------------
1 | import StepFunctionInput from "./stepFunctionInput";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { AppDispatch } from "../../store.tsx";
4 | import { setAddCardForm } from "../reducers/cardSlice"
5 | import { selectCard } from '../reducers/cardSlice.tsx'
6 |
7 |
8 | function AddCard() {
9 | const dispatch:AppDispatch = useDispatch()
10 | const card = useSelector(selectCard)
11 |
12 | const handleClick = (e: React.FormEvent) => {
13 | e.preventDefault()
14 | dispatch(setAddCardForm())
15 | }
16 |
17 | return (
18 |
19 |
22 |
23 | {card.addCardform && }
24 |
25 | )
26 | }
27 |
28 |
29 |
30 |
31 | export default AddCard;
--------------------------------------------------------------------------------
/src/landingPage/allCards.tsx:
--------------------------------------------------------------------------------
1 | import FunctionCards from './functionCards.tsx';
2 | import { RootState } from '../../store.tsx';
3 | import { addCard, card, selectCard } from '../reducers/cardSlice.tsx';
4 | // import { selectUser } from '../reducers/userSlice.tsx';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { fetchCards } from '../reducers/cardSlice.tsx';
7 | import { AppDispatch } from '../../store.tsx';
8 | import { useEffect } from 'react';
9 | import { getStepFunctions } from '../reducers/dataSlice.tsx';
10 | import { getStepFunctionList } from '../reducers/dataSlice.tsx';
11 | // console.log('from all cards');
12 | // console.log(data);
13 | // const data = useSelector(getStepFunctions);
14 |
15 | function AllCards() {
16 |
17 | const cards = useSelector(
18 | (state: RootState) => state.card.allCards
19 | ) as card[];
20 | const stepFunctionList = useSelector(
21 | (state: RootState) => state.data.stepfunctions
22 | );
23 |
24 |
25 | // const user = useSelector(selectUser);
26 | const card = useSelector(selectCard);
27 | const dispatch: AppDispatch = useDispatch();
28 |
29 |
30 | const stepfunctions = useSelector(
31 | (state: RootState) => state.data.stepfunctions
32 | );
33 |
34 | // Fetch step functions when component mounts
35 | useEffect(() => {
36 | dispatch(getStepFunctionList());
37 | }, [dispatch]);
38 |
39 | // Log descriptions of each step function
40 | useEffect(() => {
41 | if (!stepfunctions) return;
42 | stepfunctions.forEach((sf) => {
43 | // Adjust 'description' based on your actual data
44 | dispatch(addCard(sf));
45 | });
46 | }, [stepfunctions]);
47 | //end
48 |
49 | ('');
50 |
51 | //not sure how we are continually fetching data yet
52 | //this gives 'Unexpected token '<', " {
54 | if (card.status === 'idle') {
55 | dispatch(fetchCards());
56 | }
57 | }, [card.allCards.length, dispatch, card.status]);
58 |
59 | let filteredCards = [];
60 |
61 | // if (card.currentRegion) {
62 | // filteredCards = cards.filter((c) => c.region === card.currentRegion);
63 | // } else filteredCards = cards;
64 |
65 | // console.log('filteredcards: ', filteredCards);
66 |
67 | // console.log('filter', filteredCards)
68 | //if (filteredCards.length > 0) console.log(filteredCards);
69 |
70 | return (
71 |
72 | {cards.map((card, index) => (
73 |
83 | ))}
84 |
85 | );
86 | }
87 |
88 | export default AllCards;
89 |
--------------------------------------------------------------------------------
/src/landingPage/filterRegions.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch} from "react-redux";
2 | import { AppDispatch } from "../../store.tsx";
3 | import { selectCard, setCardRegion } from '../reducers/cardSlice.tsx'
4 | import { useSelector } from "react-redux";
5 |
6 | function FilterRegions() {
7 |
8 | const card = useSelector(selectCard)
9 | const dispatch: AppDispatch = useDispatch()
10 |
11 | const handleChange = (e: React.ChangeEvent) => {
12 | dispatch(setCardRegion(e.target.value))
13 | }
14 |
15 | return(
16 |
17 |
18 |
23 |
24 | )
25 | }
26 |
27 | export default FilterRegions
--------------------------------------------------------------------------------
/src/landingPage/functionCards.tsx:
--------------------------------------------------------------------------------
1 | // import { selectUser } from '../reducers/userSlice.tsx';
2 | import { useDispatch } from 'react-redux';
3 | import { AppDispatch } from '../../store.tsx';
4 | // import { fetchCards } from '../reducers/cardSlice.tsx';
5 | import { card, deleteCard } from '../reducers/cardSlice';
6 | import { useNavigate } from 'react-router-dom';
7 | import FlowChartView from '../DetailedView/FlowChartView.tsx';
8 | import { setDefinitionID, setStepFunction } from '../reducers/dataSlice.tsx';
9 |
10 | function FunctionCards({
11 | name,
12 | region,
13 | visual,
14 | view,
15 | remove,
16 | definition,
17 | id,
18 | }: card) {
19 | // const username = useSelecor(selectUser);
20 | const dispatch: AppDispatch = useDispatch();
21 | const navigate = useNavigate();
22 | //console.log(definition);
23 |
24 | // const handleDelete = (name: string) => {
25 | // dispatch(deleteCard(name));
26 | // };
27 |
28 | const handleView = (name: string) => {
29 | navigate(`/expandView`, { state: { cardName: name } });
30 | dispatch(setStepFunction(definition));
31 | dispatch(setDefinitionID(id));
32 | };
33 | // function changeView() {
34 | // navigate("/expandView")
35 | // }
36 |
37 | return (
38 |
39 |
{name}
40 |
41 | {/*
{region}
*/}
42 |
43 |
44 | {/* {visual}{' '} */}
45 |
46 |
47 |
48 |
55 |
56 | {/*
*/}
59 |
60 | );
61 | }
62 |
63 | export default FunctionCards;
64 |
65 |
--------------------------------------------------------------------------------
/src/landingPage/landingPage.tsx:
--------------------------------------------------------------------------------
1 | import NavBar from './navbar/navBar.tsx';
2 | import AllCard from './allCards.tsx';
3 | import AddCard from './addCard.tsx';
4 | import FilterRegions from './filterRegions.tsx';
5 | import {
6 | getStepFunctionList,
7 | getStepFunctions,
8 | } from '../reducers/dataSlice.tsx';
9 | import { useDispatch, useSelector } from 'react-redux';
10 | import { AppDispatch } from '../../store.tsx';
11 | import { RootState } from '../../store.tsx';
12 | import { useEffect } from 'react';
13 | // import StepFunctionInput from "./stepFunctionInput";
14 |
15 | function LandingPage() {
16 | const dispatch = useDispatch();
17 |
18 | dispatch(getStepFunctionList())
19 | .unwrap()
20 | //.then((data) => console.log(data))
21 | .then((data) => {
22 | dispatch(getStepFunctions(data));
23 | });
24 |
25 | return (
26 |
27 | {/*
*/}
28 |
29 | {/*
*/}
30 |
31 | {/* This is the Landing Page */}
32 |
33 | );
34 | }
35 |
36 | export default LandingPage;
37 |
--------------------------------------------------------------------------------
/src/landingPage/navbar/aboutPage.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | function AboutPage() {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default AboutPage;
--------------------------------------------------------------------------------
/src/landingPage/navbar/contactUs.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | function ContactUs() {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default ContactUs;
--------------------------------------------------------------------------------
/src/landingPage/navbar/homeButton.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | function HomeButton() {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default HomeButton;
--------------------------------------------------------------------------------
/src/landingPage/navbar/logout.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | function Logout() {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default Logout;
--------------------------------------------------------------------------------
/src/landingPage/navbar/navBar.tsx:
--------------------------------------------------------------------------------
1 | import AboutPage from './aboutPage.tsx';
2 | import ContactUs from './contactUs.tsx';
3 | import HomeButton from './homeButton.tsx';
4 | import Logout from './logout.tsx';
5 | //import { Link } from 'react-router-dom';
6 |
7 | function NavBar() {
8 | return (
9 | <>
10 |
11 | {/*
12 | - Home
13 | - graph
14 |
*/}
15 |
16 |
17 |
18 |
19 |
20 | >
21 | );
22 | }
23 |
24 | export default NavBar;
25 |
--------------------------------------------------------------------------------
/src/landingPage/stepFunctionInput.tsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from 'react-redux';
2 | import { AppDispatch } from '../../store.tsx';
3 | import {
4 | setAddCardFormFalse,
5 | setCardName,
6 | addCard,
7 | setCardRegion,
8 | } from '../reducers/cardSlice';
9 | import { selectCard } from '../reducers/cardSlice.tsx';
10 | import { useSelector } from 'react-redux';
11 | import { addStepFunction, appendStepFunction } from '../reducers/dataSlice.tsx';
12 | // import { card } from "../reducers/cardSlice";
13 | // import { fetchFunc } from "../reducers/cardSlice";
14 | // import { setCardLink } from "../reducers/cardSlice"
15 |
16 | function StepFunctionInput() {
17 | const dispatch: AppDispatch = useDispatch();
18 | const card = useSelector(selectCard);
19 | // const { addCardform } = useSelector(selectCard)
20 |
21 | const handleSubmit = (e: React.FormEvent) => {
22 | e.preventDefault();
23 | const form = e.target as HTMLFormElement;
24 |
25 | // const name = (form.elements.namedItem('name') as HTMLInputElement).value;
26 | const link = (form.elements.namedItem('link') as HTMLInputElement).value;
27 | const region = link.split(':states:')[1].split(':')[0];
28 | // dispatch(setCardLink(link))
29 | // dispatch(setCardName(name));
30 | // dispatch(fetchFunc(link))// not sure how we are verifying or getting info yet
31 |
32 | dispatch(setCardRegion(region));
33 |
34 | //dispatch(addCard());
35 |
36 | dispatch(addStepFunction(link))
37 | .unwrap()
38 | .then((data) => dispatch(appendStepFunction(data)));
39 |
40 | if (!card.error) {
41 | dispatch(setAddCardFormFalse());
42 | }
43 | };
44 |
45 | const handleClose = (e: React.FormEvent) => {
46 | e.preventDefault();
47 | dispatch(setAddCardFormFalse());
48 | };
49 |
50 | return (
51 |
76 | );
77 | }
78 |
79 | export default StepFunctionInput;
80 |
--------------------------------------------------------------------------------
/src/loginForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { AppDispatch } from "../store.tsx";
4 | import { selectUser, setAccessKeyID, setSecretAccessKey } from "./reducers/userSlice";
5 |
6 |
7 | function LoginForm() {
8 |
9 | // access key id, secret access key, region- dropdown
10 | const dispatch: AppDispatch = useDispatch();
11 | const user = useSelector(selectUser)
12 |
13 | const handleSubmit = (e:React.FormEvent) => {
14 | e.preventDefault();
15 | //functionality to check if access keys are valid
16 | }
17 |
18 | return (
19 |
20 | This is the Login Page
21 |
30 |
31 | )
32 | }
33 |
34 | export default LoginForm;
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import React from 'react';
4 | import App from './App.tsx';
5 | import './index.css';
6 | import { BrowserRouter } from 'react-router-dom';
7 |
8 | createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/reducers/cardSlice.tsx:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import { RootState } from '../../store.tsx';
3 | import { current } from '@reduxjs/toolkit';
4 |
5 | export interface card {
6 | name: string;
7 | region: string;
8 | visual: string;
9 | view: string;
10 | remove: string; //dom.element or react.element?
11 | // link?: string //how are we validating step functions when we are adding new function cards?
12 | definition: any;
13 | id: number;
14 | }
15 |
16 | interface cardState {
17 | allCards: card[];
18 | status: 'idle' | 'loading' | 'success' | 'failed';
19 | error: string | null;
20 | addCardform: boolean;
21 | currentLink: string;
22 | currentName: string;
23 | currentRegion: string;
24 | }
25 |
26 | const initialState: cardState = {
27 | allCards: [
28 | // {
29 | // name: 'Card 1',
30 | // region: 'US',
31 | // visual: 'Flowchart 1',
32 | // view: 'View',
33 | // remove: 'Delete',
34 | // },
35 | // {
36 | // name: 'Card 2',
37 | // region: 'US',
38 | // visual: 'Flowchart 2',
39 | // view: 'View',
40 | // remove: 'Delete',
41 | // },
42 | ],
43 | status: 'idle',
44 | error: '',
45 | addCardform: false,
46 | currentLink: '',
47 | // currentName: '',
48 | currentRegion: '',
49 | };
50 |
51 | export const fetchCards = createAsyncThunk('cards/fetchingCards', async () => {
52 | const cards = await fetch(`/home/card`, {
53 | headers: { 'content-Type': 'application/json' },
54 | });
55 | if (!cards.ok) {
56 | throw new Error('cannot fetch cards');
57 | }
58 | const homeCards = await cards.json();
59 | // console.log(homeCards)
60 | return homeCards;
61 | });
62 |
63 | export const fetchFunc = createAsyncThunk(
64 | 'cards/fetchFunc',
65 | async (link: string) => {
66 | const func = await fetch(`home/func/${link}`, {
67 | headers: { 'content-Type': 'application/json' },
68 | });
69 | if (!func.ok) {
70 | throw new Error('cannot fetch func');
71 | }
72 | const funcCard = await func.json();
73 | // console.log(funcCard)
74 | return funcCard;
75 | }
76 | );
77 |
78 | export const cardSlice = createSlice({
79 | name: 'card',
80 | initialState,
81 | reducers: {
82 | setAddCardForm: (state) => {
83 | state.addCardform = true;
84 | },
85 | setAddCardFormFalse: (state) => {
86 | state.addCardform = false;
87 | },
88 | setCardLink: (state, action) => {
89 | state.currentLink = action.payload;
90 | },
91 | setCardName: (state, action) => {
92 | state.currentName = action.payload;
93 | },
94 | setCardRegion: (state, action) => {
95 | state.currentRegion = action.payload;
96 | },
97 | addCard: (state, action) => {
98 | let exists = false;
99 | state.allCards.forEach((card) => {
100 | if (card.name === action.payload.name) {
101 | exists = true;
102 | }
103 | });
104 |
105 | const newCards = [];
106 | //console.log(exists);
107 | if (exists) {
108 | state.error = 'name already exists';
109 | return;
110 | }
111 | const newCard: card = {
112 | name: action.payload.name,
113 | region: state.currentRegion,
114 | visual: 'chart',
115 | view: 'View',
116 | remove: 'delete',
117 | //link: state.currentLink
118 | definition: action.payload.definition,
119 | id: 0,
120 | };
121 | state.allCards.push(newCard);
122 | state.currentName = '';
123 | state.currentLink = '';
124 | state.error = '';
125 | },
126 | deleteCard: (state, action) => {
127 | state.allCards = state.allCards.filter(
128 | (card) => card.name !== action.payload
129 | );
130 | },
131 | },
132 | extraReducers: (builder) => {
133 | builder
134 | .addCase(fetchCards.pending, (state) => {
135 | state.status = 'loading';
136 | })
137 | .addCase(fetchCards.fulfilled, (state, action) => {
138 | state.status = 'success';
139 | state.allCards = action.payload;
140 | console.log('fetchcards success', action.payload);
141 | })
142 | .addCase(fetchCards.rejected, (state, action) => {
143 | state.status = 'failed';
144 | state.error = action.error.message ?? 'failed to fetch cards';
145 | });
146 | },
147 | });
148 |
149 | export const {
150 | setAddCardForm,
151 | setAddCardFormFalse,
152 | setCardLink,
153 | setCardName,
154 | addCard,
155 | deleteCard,
156 | setCardRegion,
157 | } = cardSlice.actions;
158 |
159 | export const selectCard = (state: RootState) => state.card;
160 | export default cardSlice.reducer;
161 |
--------------------------------------------------------------------------------
/src/reducers/dataSlice.tsx:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 | import { RootState } from '../../store.tsx';
3 | import { StateEnteredEventDetailsFilterSensitiveLog } from '@aws-sdk/client-sfn';
4 | import { current } from '@reduxjs/toolkit';
5 |
6 | type stepfunction = {
7 | definition?: object;
8 | name?: string;
9 | step_function_id?: number;
10 | };
11 |
12 | interface dataState {
13 | stepfunctions: stepfunction[];
14 | currentDefinition: object | undefined;
15 | currentDefinitionID: number;
16 | latencies: [
17 | {
18 | steps: any;
19 | startTime?: any;
20 | stepFunctionAverageLatency?: any;
21 | }
22 | ];
23 | latency: any;
24 | chartLatencies: number[];
25 | time: string;
26 | bubblePopup: boolean;
27 | bubbleName: string;
28 | }
29 |
30 | const initialState: dataState = {
31 | stepfunctions: [],
32 | currentDefinition: JSON.parse(
33 | localStorage.getItem('currentDefinition') || '{}'
34 | ),
35 | currentDefinitionID:
36 | JSON.parse(localStorage.getItem('currentDefinitionID') /*|| '0'*/) || 0,
37 | latencies: JSON.parse(
38 | localStorage.getItem('latencies') /*|| '[{steps: null}]'*/
39 | ) || [{ steps: null }],
40 | latency: {},
41 | chartLatencies: [],
42 | time:
43 | JSON.parse(localStorage.getItem('timeToggle') /*|| 'hours'*/) || 'hours',
44 | bubblePopup: false,
45 | bubbleName: '',
46 | };
47 |
48 | export const dataSlice = createSlice({
49 | name: 'data',
50 | initialState,
51 | reducers: {
52 | setLatencies: (state, action) => {
53 | //console.log(action.payload);
54 | state.latencies = action.payload;
55 | if (state.latencies.length > 0) state.latency = state.latencies[0];
56 | localStorage.setItem('latencies', JSON.stringify(action.payload));
57 | },
58 | setLatency: (state, action) => {
59 | if (state.latencies.length > 0)
60 | state.latency = state.latencies[action.payload];
61 | },
62 | setStepFunction: (state, action) => {
63 | // console.log('Adding step function definion');
64 | state.currentDefinition = action.payload;
65 | localStorage.setItem('currentDefinition', JSON.stringify(action.payload));
66 |
67 | // console.log(state.currentDefinition);
68 | },
69 | setDefinitionID: (state, action) => {
70 | state.currentDefinitionID = action.payload;
71 | localStorage.setItem(
72 | 'currentDefinitionID',
73 | JSON.stringify(action.payload)
74 | );
75 | },
76 | getStepFunctions: (state, action) => {
77 | state.stepfunctions = action.payload;
78 | // if (state.stepfunctions)
79 | // state.currentDefinition = state.stepfunctions[0].definition;
80 | },
81 | appendStepFunction: (state, action) => {
82 | state.stepfunctions.push(action.payload);
83 | },
84 | setChartLatencies: (state, action) => {
85 | if (action.payload) {
86 | const newChart: any[] = [];
87 | state.latencies.forEach((ele) => {
88 | if (ele.steps) {
89 | if (ele.steps[action.payload])
90 | newChart.push(ele.steps[action.payload].average);
91 | } else newChart.push(null);
92 | });
93 | state.chartLatencies = newChart;
94 | }
95 | },
96 | setTimeToggle: (state, action) => {
97 | state.time = action.payload;
98 | localStorage.setItem('timeToggle', JSON.stringify(action.payload));
99 | },
100 | setBubblePopup: (state, action) => {
101 | state.bubblePopup = action.payload;
102 | },
103 | setBubbleName: (state, action) => {
104 | state.bubbleName = action.payload;
105 | },
106 | },
107 | });
108 |
109 | export const getStepFunctionList = createAsyncThunk(
110 | 'data/getStepFunctions',
111 | async () => {
112 | const res = await fetch('/api/step-functions');
113 | if (!res.ok) {
114 | throw new Error('Cannot fetch stepfunctions');
115 | }
116 | const stepfunctions: stepfunction[] = await res.json();
117 | return stepfunctions;
118 | }
119 | );
120 |
121 | export const getLatencies = createAsyncThunk(
122 | 'data/getLatencies',
123 | async ({ id, time }: { id: number; time: string }) => {
124 | const res = await fetch(`/api/average-latencies/${id}/${time}`);
125 | if (!res.ok) {
126 | throw new Error('Cannot fetch stepfunctions');
127 | }
128 | const latencies = await res.json();
129 | return latencies;
130 | }
131 | );
132 |
133 | export const addStepFunction = createAsyncThunk(
134 | 'data/addStepFunction',
135 | async (arn: string) => {
136 | const res = await fetch('/api/step-functions/addStepFunction', {
137 | method: 'POST',
138 | headers: { 'Content-Type': 'application/json' },
139 | body: JSON.stringify({
140 | arn: arn,
141 | }),
142 | });
143 | if (!res.ok) {
144 | throw new Error('Connot create new stepfunction');
145 | }
146 | const newfunction: stepfunction = await res.json();
147 | return newfunction;
148 | }
149 | );
150 |
151 | export const {
152 | setLatency,
153 | setLatencies,
154 | setStepFunction,
155 | setDefinitionID,
156 | getStepFunctions,
157 | appendStepFunction,
158 | setChartLatencies,
159 | setTimeToggle,
160 | setBubblePopup,
161 | setBubbleName,
162 | } = dataSlice.actions;
163 |
164 | export const selectData = (state: RootState) => state.data;
165 |
166 | export default dataSlice.reducer;
167 |
--------------------------------------------------------------------------------
/src/reducers/userSlice.tsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { RootState } from '../../store.tsx';
3 |
4 | const initialState = {
5 | accessKeyID: '',
6 | secretAccessKey: '',
7 | region: 'Select one',
8 | allCards: {},
9 | };
10 |
11 | export const userSlice = createSlice({
12 | name: 'user',
13 | initialState,
14 | reducers: {
15 | setAccessKeyID: (state, action) => {
16 | state.accessKeyID = action.payload
17 | },
18 | setSecretAccessKey: (state, action) => {
19 | state.secretAccessKey = action.payload
20 | },
21 | setRegion: (state, action) => {
22 | state.region = action.payload
23 | }
24 | }
25 | })
26 |
27 | export const {
28 | setAccessKeyID,
29 | setSecretAccessKey,
30 | setRegion
31 | } = userSlice.actions;
32 |
33 | export const selectUser = (state:RootState) => state.user
34 | export default userSlice.reducer
--------------------------------------------------------------------------------
/src/signUpForm.tsx:
--------------------------------------------------------------------------------
1 | import React,{useState} from "react";
2 |
3 |
4 | function SignUpForm() {
5 | const [username, setUsername] = useState('');
6 | const [password, setPassword] = useState('');
7 |
8 | return (
9 |
10 | This is the Sign Up Page
11 |
12 | )
13 | }
14 |
15 | export default SignUpForm;
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/store.tsx:
--------------------------------------------------------------------------------
1 | import { configureStore, /*combineReducers*/ } from "@reduxjs/toolkit";
2 | import userReducer from './src/reducers/userSlice';
3 | import cardReducer from "./src/reducers/cardSlice";
4 | import dataReducer from "./src/reducers/dataSlice";
5 | // import { persistReducer, persistStore } from 'redux-persist'
6 | // import storage from 'redux-persist/lib/storage';
7 |
8 | // const persistConfig = {
9 | // key: 'root',
10 | // storage,
11 | // };
12 |
13 | // const rootReducer = combineReducers({
14 | // user: userReducer,
15 | // card: cardReducer,
16 | // data: dataReducer,
17 | // });
18 |
19 | // const persistedReducer = persistReducer(persistConfig, rootReducer);
20 |
21 | const store = configureStore({
22 | reducer: {
23 | user: userReducer,
24 | card: cardReducer,
25 | data: dataReducer,
26 | }
27 | // reducer: persistedReducer,
28 | })
29 |
30 | export type RootState = ReturnType;
31 |
32 | export type AppDispatch = typeof store.dispatch
33 |
34 | // export const persistor = persistStore(store);
35 | export default store
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | container: {
6 | center: true,
7 | },
8 | extend: {
9 | boxShadow: {
10 | inner: 'inset 10px 10px 10px 0 rgb(0 0 0 / 0.05);',
11 | glow: '0 0 0 2px rgb(147 51 234), 0 0 0 3px white, 0 0 0 5px rgb(147 51 234);',
12 | },
13 | size: {
14 | 100: '28rem',
15 | 124: '50rem',
16 | },
17 | },
18 | },
19 | plugins: [],
20 | };
21 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 | "tsx": "react-tsx",
17 |
18 | /* Linting */
19 | // "strict": true,
20 | //"noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": ["src"],
25 | "exclude": ["node_modules", "server"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.app.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./src/App.tsx","./src/loginForm.tsx","./src/main.tsx","./src/signUpForm.tsx","./src/vite-env.d.ts","./src/DetailedView/BackButton.tsx","./src/DetailedView/DataContainer.tsx","./src/DetailedView/DataVisualization.tsx","./src/DetailedView/DetailedView.tsx","./src/DetailedView/DetailedViewUI.tsx","./src/DetailedView/FlowChart.tsx","./src/DetailedView/FlowChartBubble.tsx","./src/DetailedView/FlowChartDataSelector.tsx","./src/DetailedView/FlowChartView.tsx","./src/DetailedView/HeatmapChart.tsx","./src/DetailedView/StepDataVisualization.tsx","./src/DetailedView/TimePeriodToggle.tsx","./src/DetailedView/TimeSlice.tsx","./src/DetailedView/TimeSlider.tsx","./src/DetailedView/TimeToggle.tsx","./src/landingPage/addCard.tsx","./src/landingPage/allCards.tsx","./src/landingPage/filterRegions.tsx","./src/landingPage/functionCards.tsx","./src/landingPage/landingPage.tsx","./src/landingPage/stepFunctionInput.tsx","./src/landingPage/navbar/aboutPage.tsx","./src/landingPage/navbar/contactUs.tsx","./src/landingPage/navbar/homeButton.tsx","./src/landingPage/navbar/logout.tsx","./src/landingPage/navbar/navBar.tsx","./src/reducers/cardSlice.tsx","./src/reducers/dataSlice.tsx","./src/reducers/userSlice.tsx"],"errors":true,"version":"5.6.3"}
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | //"types": ["vitest/globals"],
4 | "typeRoots": ["./node_modules/@types"]
5 | },
6 | "files": [],
7 | "references": [
8 | { "path": "./tsconfig.node.json" },
9 | { "path": "./tsconfig.server.json" },
10 | { "path": "./tsconfig.knex.json" },
11 | { "path": "./tsconfig.app.json" }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.knex.json:
--------------------------------------------------------------------------------
1 | // {
2 | // "extends": "./tsconfig.json",
3 | // "compilerOptions": {
4 | // "target": "ES2020",
5 | // "module": "CommonJS",
6 | // "outDir": "./dist/database",
7 | // "rootDir": "./database",
8 | // "esModuleInterop": true,
9 | // "allowSyntheticDefaultImports": true
10 | // },
11 | // "include": ["database"],
12 | // "exclude": ["docs", "seeds"]
13 | // }
14 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.node.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {
2 | "root": [
3 | "./vite.config.ts"
4 | ],
5 | "errors": true,
6 | "version": "5.6.3"
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "module": "CommonJS",
6 | "outDir": "./dist",
7 | "rootDir": "./",
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true
10 | },
11 | "include": [
12 | "server/**/*",
13 | "workers/**/*",
14 | "database/migrations",
15 | "knexfile.ts"
16 | ],
17 | "exclude": ["node_modules", "src", "database/docs"]
18 | }
19 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | import { defineConfig } from 'vite';
6 | import react from '@vitejs/plugin-react-swc';
7 | import { stopCoverage } from 'v8';
8 | import { report } from 'process';
9 |
10 | // https://vitejs.dev/config/
11 | export default defineConfig({
12 | // test: {
13 | // include: ['./test/test.jsx'],
14 | // environment: 'jsdom',
15 | // globals: true,
16 | // css: true,
17 | // // setupFiles: ['./setupTests.js'],
18 | // // setupFiles: "./test/test.ts"
19 | // },
20 | plugins: [react()],
21 | server: {
22 | proxy: {
23 | '/api': {
24 | target: 'http://localhost:3000', // Replace with your Express server port
25 | changeOrigin: true,
26 | },
27 | },
28 | },
29 | test: {
30 | coverage: { include: ['src/**/*.{tsx,ts}'] },
31 | stopCoverage: {
32 | reportsDirectory: './src',
33 | report: ['text', 'html'],
34 | },
35 | include: ['./__test__/**.tsx'],
36 | environment: 'jsdom',
37 | globals: true,
38 | css: true,
39 | // setupFiles: ['./setupTests.js'],
40 | // setupFiles: "./test/test.ts"
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/workers/StepFunction.ts:
--------------------------------------------------------------------------------
1 | import type { StepsByStepFunctionId } from "../server/models/types";
2 | import { FormattedSteps } from "./types";
3 | import { StepsTable } from "../server/models/types";
4 |
5 | export type StepFunction = {
6 | id: number;
7 | stepIds: number[];
8 | stepRows: StepsByStepFunctionId[];
9 | steps: FormattedSteps;
10 | setStepIdArray: (this: StepFunction) => void;
11 | formatSteps: (this: StepFunction, stepRows: StepsTable[]) => FormattedSteps;
12 | };
13 |
14 | /**
15 | * Creates step function objects in functional style to conform with the rest
16 | * of the codebase's functional style.
17 | * @param stepRows Rows of step data from the database steps table for this step
18 | * function
19 | * @returns stepFunction object
20 | */
21 | export function createStepFunction(stepRows: StepsByStepFunctionId[]) {
22 | const stepFunction = Object.create(stepFunctionPrototype);
23 | stepFunction.setStepIdArray(stepRows);
24 | stepFunction.formatSteps(stepRows);
25 | return stepFunction;
26 | }
27 | const stepFunctionPrototype = {
28 | /**
29 | * Gets an array of step ids to filter database queries based on id
30 | * @param this StepFunction object
31 | * @param stepRows Rows of step data from the database steps table for this step
32 | * function
33 | * @returns undefined (void)
34 | */
35 | setStepIdArray(this: StepFunction, stepRows: StepsTable[]): void {
36 | this.stepIds = stepRows.map((el) => el.step_id);
37 | return;
38 | },
39 | /**
40 | * This function formats the steps into an object with keys as the step names
41 | * for easier lookup when parsing logs
42 | * @param this StepFunction object
43 | * @param stepRows Rows of step data from the database steps table for this step
44 | * function
45 | * @returns FormattedSteps object
46 | */
47 | formatSteps(this: StepFunction, stepRows: StepsTable[]): FormattedSteps {
48 | const steps: FormattedSteps = {};
49 | for (const step of stepRows) {
50 | steps[step.name] = {
51 | type: step.type,
52 | stepId: step.step_id,
53 | };
54 | }
55 | this.steps = steps;
56 | return steps;
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/workers/Tracker.ts:
--------------------------------------------------------------------------------
1 | import moment, { Moment } from "moment";
2 | import stepFunctionTrackersModel from "../server/models/stepFunctionTrackersModel";
3 | import { TrackerStepFunctionsJoinTable } from "../server/models/types";
4 | import { StepFunction } from "./StepFunction";
5 |
6 | type TimeSegment = {
7 | startTime: Moment;
8 | endTime: Moment;
9 | isComplete?: boolean;
10 | };
11 |
12 | export type Tracker = {
13 | trackerDbRow: TrackerStepFunctionsJoinTable;
14 | stepFunction: StepFunction;
15 | logGroupArn: string;
16 | logGroupName: string;
17 | logGroupRegion: string;
18 | logStreamNamePrefix: string;
19 | currentStartTime: Moment;
20 | currentEndTime: Moment;
21 | timeSegment: TimeSegment[];
22 | nextToken: string | undefined;
23 | setTimeSegments: (this: Tracker) => void;
24 | setLogGroup: (this: Tracker, logGroupArn: string) => void;
25 | decrementCurrentTimes: (this: Tracker) => Promise;
26 | isFinished: (this: Tracker) => Promise;
27 | updateTrackerTimes: (
28 | this: Tracker,
29 | startTime: Moment,
30 | endTime: Moment
31 | ) => Promise;
32 | };
33 |
34 | /**
35 | * Creates a Tracker object to hold data needed to queue logs for processing
36 | * @param stepFunction StepFunction object to attach to this tracker
37 | * @param trackerDbRow Rows of data received from the database related to this
38 | * tracker
39 | * @returns
40 | */
41 | export function createTracker(
42 | stepFunction: StepFunction,
43 | trackerDbRow: TrackerStepFunctionsJoinTable
44 | ) {
45 | const tracker = Object.create(trackerPrototype);
46 | tracker.trackerDbRow = trackerDbRow;
47 | tracker.stepFunction = stepFunction;
48 | tracker.setLogGroup(trackerDbRow.log_group_arn);
49 | tracker.logGroupRegion = trackerDbRow.log_group_arn.split(":")[3];
50 | tracker.logStreamNamePrefix = `states/${trackerDbRow?.name}`;
51 | tracker.setTimeSegments();
52 | tracker.nextToken = undefined;
53 |
54 | return tracker;
55 | }
56 |
57 | const trackerPrototype = {
58 | /**
59 | * Removes the ending ':*' characters from the log if they are present.
60 | * Also reads the log group name from the arn and stores it on the object.
61 | * @param this Tracker object
62 | * @param logGroupArn The full raw log group arn
63 | * @returns undefined
64 | */
65 | setLogGroup(this: Tracker, logGroupArn: string): void {
66 | if (logGroupArn.endsWith(":*")) logGroupArn = logGroupArn.slice(0, -2);
67 | this.logGroupArn = logGroupArn;
68 | this.logGroupName = logGroupArn.split("log-group:")[1];
69 | return;
70 | },
71 | /**
72 | * Splits the logs to scan up into segments. Produces only one segment.
73 | * Also stores the current time period to begin processing logs.
74 | * @param this Tracker object
75 | * @returns undefined
76 | */
77 | setTimeSegments(this: Tracker): void {
78 | if (
79 | this.trackerDbRow.newest_execution_time === null &&
80 | this.trackerDbRow.oldest_execution_time === null
81 | ) {
82 | this.timeSegment = [
83 | {
84 | startTime: moment(this.trackerDbRow.tracker_start_time).utc(),
85 | endTime: moment().subtract(2, "hours").endOf("hour").utc(),
86 | },
87 | ];
88 |
89 | this.currentStartTime = moment()
90 | .subtract(2, "hours")
91 | .startOf("hour")
92 | .utc();
93 | this.currentEndTime = moment().subtract(2, "hours").endOf("hour").utc();
94 | } else {
95 | this.timeSegment = [
96 | {
97 | startTime: moment(this.trackerDbRow.newest_execution_time).utc(),
98 | endTime: moment().subtract(2, "hours").endOf("hour").utc(),
99 | },
100 | ];
101 | this.currentStartTime = moment()
102 | .subtract(2, "hours")
103 | .startOf("hour")
104 | .utc();
105 | this.currentEndTime = moment().subtract(2, "hours").endOf("hour").utc();
106 | }
107 | },
108 | /**
109 | * Subtracts an hour from the current time object properties
110 | * @param this Tracker object
111 | * @returns Promise
112 | */
113 | async decrementCurrentTimes(this: Tracker): Promise {
114 | this.currentEndTime.subtract(1, "hour");
115 | this.currentStartTime.subtract(1, "hour");
116 | return;
117 | },
118 | /**
119 | * Tests to see if the tracker has processed all within the time stegment
120 | * @param this Tracker object
121 | * @returns true | false
122 | */
123 | async isFinished(this: Tracker): Promise {
124 | if (this.currentStartTime.isBefore(this.timeSegment[0].startTime)) {
125 | return true;
126 | }
127 | return false;
128 | },
129 | /**
130 | * Updates the tracker table in the database with the newest and oldest logs
131 | * scanned if appropriate.
132 | * @param this Tracker object
133 | * @param startTime Moment object for the oldest update
134 | * @param endTime Moment object for the newest update
135 | */
136 | async updateTrackerTimes(
137 | this: Tracker,
138 | startTime: Moment,
139 | endTime: Moment
140 | ): Promise {
141 | if (
142 | this.trackerDbRow.newest_execution_time === null ||
143 | moment(this.trackerDbRow.newest_execution_time).isBefore(endTime)
144 | ) {
145 | await stepFunctionTrackersModel.updateNewestExecutionTime(
146 | this.trackerDbRow.tracker_id,
147 | endTime.toISOString()
148 | );
149 | }
150 |
151 | if (
152 | this.trackerDbRow.oldest_execution_time === null ||
153 | moment(this.trackerDbRow.oldest_execution_time).isAfter(startTime)
154 | ) {
155 | await stepFunctionTrackersModel.updateOldestExecutionTime(
156 | this.trackerDbRow.tracker_id,
157 | startTime.toISOString()
158 | );
159 | }
160 | },
161 | };
162 |
--------------------------------------------------------------------------------
/workers/cloudWatchCronJob.ts:
--------------------------------------------------------------------------------
1 | import "dotenv/config";
2 | import nodeCron, { CronJob } from "node-cron";
3 | import Bottleneck from "bottleneck";
4 | import getCloudWatchData from "./getCloudWatchData";
5 |
6 | const jobBottleneck = new Bottleneck({
7 | maxConcurrent: 1,
8 | minTime: 0,
9 | });
10 |
11 | const runJob = async (trackerId: number | undefined): Promise => {
12 | console.log("Scheduling CloudWatch job.");
13 | await jobBottleneck.schedule(async () => {
14 | try {
15 | console.log("Running CloudWatch job.");
16 | await getCloudWatchData(trackerId);
17 | console.log("Completed CloudWatch job.");
18 | } catch (err) {
19 | console.error(`Error running job ${err}`);
20 | }
21 | });
22 | };
23 |
24 | // run on 5th minute of every hour
25 | const cronJob: CronJob = nodeCron.schedule("5 * * * *", () =>
26 | runJob(undefined)
27 | );
28 | console.log("Cron job scheduled for the 5th minute of every hour.");
29 |
30 | export default { runJob, cronJob };
31 |
--------------------------------------------------------------------------------
/workers/executions.ts:
--------------------------------------------------------------------------------
1 | import stepFunctionAverageLatenciesModel from "../server/models/stepFunctionAverageLatenciesModel";
2 | import stepAverageLatenciesModel from "../server/models/stepAverageLatenciesModel";
3 | import type { StepCurrentLatencies, LatencyData } from "./types";
4 | import type {
5 | StepAverageLatencies,
6 | AverageLatencies,
7 | } from "../server/models/types";
8 | import type { Moment } from "moment";
9 |
10 | /**
11 | * Adds latencies to the database for step functions and each step, whether
12 | * updating existing rows or adding new rows of data.
13 | * @param latencyData Object with latency data per execution of the step function
14 | * @param stepFunctionId Id of the step function as referenced in the database
15 | * @param stepIds Array step ids which belong to this step function
16 | * @param startTime The start time of the hour for this data
17 | * @param endTime The end time of the hour for this data
18 | */
19 | const addLatenciesToDatabase = async (
20 | latencyData: LatencyData,
21 | stepFunctionId: number,
22 | stepIds: number[],
23 | startTime: Moment,
24 | endTime: Moment
25 | ): Promise => {
26 | // get average data for the step function and steps for this hour
27 | const [stepFunctionCurrentLatencyData] =
28 | await stepFunctionAverageLatenciesModel.getStepFunctionLatencies(
29 | stepFunctionId,
30 | startTime.toISOString(),
31 | endTime.toISOString()
32 | );
33 |
34 | const stepsCurrentLatencyRows =
35 | await stepAverageLatenciesModel.getHourlyLatenciesBetweenTimes(
36 | stepIds,
37 | startTime.toISOString(),
38 | endTime.toISOString()
39 | );
40 |
41 | await upsertStepFunctionLatency(
42 | stepFunctionCurrentLatencyData,
43 | latencyData,
44 | stepFunctionId,
45 | startTime,
46 | endTime
47 | );
48 | if (Object.keys(latencyData.steps).length > 0) {
49 | await upsertStepLatencies(
50 | stepsCurrentLatencyRows,
51 | latencyData,
52 | startTime,
53 | endTime
54 | );
55 | }
56 | };
57 |
58 | /**
59 | * Inserts or Updates the step function average latencies in the database with
60 | * the most recent calculated values.
61 | * @param stepFunctionCurrentLatencyData Row of database information for the
62 | * overall step function latency from step_function_average_latencies table
63 | * @param latencyData New latency data from cloudwatch logs
64 | * @param stepFunctionId Id of the step function as stored in the database
65 | * @param startTime The start time of the hour for this data
66 | * @param endTime The end time of the hour for this data
67 | */
68 | const upsertStepFunctionLatency = async (
69 | stepFunctionCurrentLatencyData: AverageLatencies,
70 | latencyData: LatencyData,
71 | stepFunctionId: number,
72 | startTime: Moment,
73 | endTime: Moment
74 | ) => {
75 | if (stepFunctionCurrentLatencyData) {
76 | const [newAverage, newExecutions] = await calculateNewLatencyData(
77 | stepFunctionCurrentLatencyData.average,
78 | stepFunctionCurrentLatencyData.executions,
79 | latencyData.stepFunctionLatencySum / 1000,
80 | latencyData.executions
81 | );
82 |
83 | await stepFunctionAverageLatenciesModel.updateStepFunctionLatency(
84 | stepFunctionCurrentLatencyData.latency_id,
85 | newAverage,
86 | newExecutions
87 | );
88 | } else {
89 | await stepFunctionAverageLatenciesModel.insertStepFunctionLatencies([
90 | {
91 | step_function_id: stepFunctionId,
92 | start_time: startTime.toISOString(),
93 | end_time: endTime.toISOString(),
94 | average:
95 | latencyData.stepFunctionLatencySum / latencyData.executions / 1000,
96 | executions: latencyData.executions,
97 | },
98 | ]);
99 | }
100 | };
101 |
102 | /**
103 | * Inserts or Updates step average latencies in the database with most recent
104 | * calculated values.
105 | * @param stepsCurrentLatencyRows Rows of database information for each step
106 | * which includes its currently latency information
107 | * @param latencyData New latency data object created from cloudwatch logs
108 | * @param startTime The start time of the hour this data is for
109 | * @param endTime The end time of the hour this data is for
110 | */
111 | const upsertStepLatencies = async (
112 | stepsCurrentLatencyRows: StepAverageLatencies[],
113 | latencyData: LatencyData,
114 | startTime: Moment,
115 | endTime: Moment
116 | ) => {
117 | const stepsCurrentLatencyObject: StepCurrentLatencies = {};
118 | stepsCurrentLatencyRows.forEach((row) => {
119 | stepsCurrentLatencyObject[row.step_id] = {
120 | average: row.average,
121 | executions: row.executions,
122 | latencyId: row.latency_id,
123 | };
124 | });
125 |
126 | if (stepsCurrentLatencyRows.length > 0) {
127 | for (const stepId of Object.keys(latencyData.steps)) {
128 | if (stepsCurrentLatencyObject[stepId] !== undefined) {
129 | const [newAverage, newExecutions] = await calculateNewLatencyData(
130 | stepsCurrentLatencyObject[stepId].average,
131 | stepsCurrentLatencyObject[stepId].executions,
132 | Number(latencyData.steps[stepId].sum) / 1000,
133 | Number(latencyData.steps[stepId].executions)
134 | );
135 | await stepAverageLatenciesModel.updateStepAverageLatency(
136 | stepsCurrentLatencyObject[stepId].latencyId,
137 | newAverage,
138 | newExecutions
139 | );
140 | } else {
141 | const average =
142 | Number(latencyData.steps[stepId].sum) /
143 | Number(latencyData.steps[stepId].executions) /
144 | 1000; // convert from milliseconds to seconds
145 |
146 | await stepAverageLatenciesModel.insertStepAverageLatencies([
147 | {
148 | step_id: Number(stepId),
149 | average,
150 | executions: Number(latencyData.steps[stepId].executions),
151 | start_time: startTime.toISOString(),
152 | end_time: endTime.toISOString(),
153 | },
154 | ]);
155 | }
156 | }
157 | } else {
158 | for (const stepId of Object.keys(latencyData.steps)) {
159 | const average =
160 | Number(latencyData.steps[stepId].sum) /
161 | Number(latencyData.steps[stepId].executions) /
162 | 1000; // convert from milliseconds to seconds
163 |
164 | await stepAverageLatenciesModel.insertStepAverageLatencies([
165 | {
166 | step_id: Number(stepId),
167 | average,
168 | executions: Number(latencyData.steps[stepId].executions),
169 | start_time: startTime.toISOString(),
170 | end_time: endTime.toISOString(),
171 | },
172 | ]);
173 | }
174 | }
175 | };
176 |
177 | /**
178 | * Calculates latency by combining old data with new data
179 | * @param oldAverage Existing average from
180 | * @param oldExecutions Existing number of executions from the database
181 | * @param newSum New latency sum from cloudwatch logs
182 | * @param newExecutions New execution sum from cloudwatch logs
183 | * @returns Array of [new average, new total executions]
184 | */
185 | const calculateNewLatencyData = async (
186 | oldAverage: number,
187 | oldExecutions: number,
188 | newSum: number,
189 | newExecutions: number
190 | ): Promise => {
191 | const oldSum = Number(oldAverage) * Number(oldExecutions);
192 | const newAverage =
193 | (Number(newSum) + Number(oldSum)) /
194 | (Number(newExecutions) + Number(oldExecutions));
195 | const totalExecutions = Number(newExecutions) + Number(oldExecutions);
196 | return [newAverage, totalExecutions];
197 | };
198 |
199 | const executionsObject = {
200 | addLatenciesToDatabase,
201 | upsertStepFunctionLatency,
202 | upsertStepLatencies,
203 | calculateNewLatencyData,
204 | };
205 | export default executionsObject;
206 |
--------------------------------------------------------------------------------
/workers/logs.ts:
--------------------------------------------------------------------------------
1 | import type { Executions, LatencyData, FormattedSteps } from "./types";
2 |
3 | /**
4 | * Calculates the overall latency and individual step latencies for each
5 | * execution. Stores incomplete executions for later processing.
6 | * @param executions Object with data on all executions collected from the logs.
7 | * Data is keyed by execution arn.
8 | * @param steps Steps object for all steps this step function uses, organized by
9 | * step name as keys.
10 | * @returns [LatencyData, Executions]
11 | */
12 | const calculateLogLatencies = async (
13 | executions: Executions,
14 | steps: FormattedSteps
15 | ): Promise<[LatencyData, Executions]> => {
16 | const latencyData: LatencyData = {
17 | executions: 0,
18 | stepFunctionLatencySum: 0,
19 | steps: {},
20 | };
21 |
22 | const endTypesSet = new Set([
23 | "ExecutionSucceeded",
24 | "ExecutionFailed",
25 | "ExecutionAborted",
26 | "ExecutionTimedOut",
27 | ]);
28 |
29 | const incompleteExecutions: Executions = {};
30 |
31 | for (const executionArn in executions) {
32 | const stepBuffer = {};
33 | const events = executions[executionArn].events;
34 |
35 | if (!endTypesSet.has(events[events.length - 1].type)) {
36 | incompleteExecutions[executionArn] = executions[executionArn];
37 | incompleteExecutions[executionArn].isStillRunning = true;
38 | continue;
39 | }
40 | if (executions[executionArn].eventsFound !== events.length) {
41 | incompleteExecutions[executionArn] = executions[executionArn];
42 | incompleteExecutions[executionArn].hasMissingEvents = true;
43 | continue;
44 | }
45 |
46 | latencyData.executions++;
47 |
48 | latencyData.stepFunctionLatencySum +=
49 | events[events.length - 1].timestamp - events[0].timestamp;
50 |
51 | for (const event of events) {
52 | // these are event types which do not have an associated latency
53 | if (event.type === "ExecutionStarted") continue;
54 | if (event.type === "ExecutionSucceeded") continue;
55 | if (event.name === undefined || steps[event.name] === undefined) continue;
56 | if (
57 | event.type === "FailStateEntered" ||
58 | event.type === "SucessStateEntered"
59 | )
60 | continue;
61 |
62 | if (event.type.endsWith("Entered")) {
63 | if (stepBuffer[event.name] === undefined) {
64 | stepBuffer[event.name] = [{ timestamp: event.timestamp }];
65 | } else {
66 | stepBuffer[event.name].push([{ timestamp: event.timestamp }]);
67 | }
68 | } else if (event.type.endsWith("Exited")) {
69 | const step = stepBuffer[event.name].shift();
70 | const latency = event.timestamp - step.timestamp;
71 |
72 | const stepId = steps[event.name].stepId;
73 |
74 | if (latencyData.steps[stepId] !== undefined) {
75 | latencyData.steps[stepId].executions++;
76 | latencyData.steps[stepId].sum += latency;
77 | } else {
78 | latencyData.steps[stepId] = {
79 | executions: 1,
80 | sum: latency,
81 | };
82 | }
83 | }
84 | }
85 | }
86 | return [latencyData, incompleteExecutions];
87 | };
88 |
89 | const logs = {
90 | calculateLogLatencies,
91 | };
92 |
93 | export default logs;
94 |
--------------------------------------------------------------------------------
/workers/types.ts:
--------------------------------------------------------------------------------
1 | export interface Executions {
2 | // step function execution arn as key
3 | [executionArn: string]: {
4 | logStreamName: string;
5 | events: Event[];
6 | eventsFound?: number;
7 | isStillRunning?: boolean;
8 | hasMissingEvents?: boolean;
9 | };
10 | }
11 | export interface Event {
12 | id: number;
13 | type: string;
14 | name?: string; // some events like start and end dont have a name
15 | timestamp: number; // epoch milliseconds
16 | eventId: string; // actually a long string of numbers
17 | }
18 |
19 | export interface FormattedSteps {
20 | [stepName: string]: {
21 | type: string;
22 | stepId: number;
23 | };
24 | }
25 |
26 | export interface LatencyData {
27 | executions: number;
28 | stepFunctionLatencySum: number;
29 | steps: {
30 | [stepName: string]: {
31 | executions: number;
32 | sum: number;
33 | };
34 | };
35 | }
36 |
37 | export interface StepCurrentLatencies {
38 | [stepName: string]: {
39 | average: number;
40 | executions: number;
41 | latencyId: number;
42 | };
43 | }
44 |
45 | export type JobQueue = () => Promise;
46 |
--------------------------------------------------------------------------------