├── .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 |
  1. Create an .env file in the root folder directory. Make sure it is ignored by 13 | git. 14 |
  2. 15 |
  3. 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 |
  4. 23 |
  5. npm run setup:database to create the initial database called time_climb. 24 |
  6. 25 |
  7. 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 |
200 |
201 |
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 |
6 | About 7 |
8 | ) 9 | } 10 | 11 | export default AboutPage; -------------------------------------------------------------------------------- /src/landingPage/navbar/contactUs.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | function ContactUs() { 4 | return ( 5 |
6 | Contact Us 7 |
8 | ) 9 | } 10 | 11 | export default ContactUs; -------------------------------------------------------------------------------- /src/landingPage/navbar/homeButton.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | function HomeButton() { 4 | return ( 5 |
6 | Home 7 |
8 | ) 9 | } 10 | 11 | export default HomeButton; -------------------------------------------------------------------------------- /src/landingPage/navbar/logout.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | function Logout() { 4 | return ( 5 |
6 | Logout 7 |
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 |
52 |
53 | {/* 54 | */} 60 | 61 | 67 | 68 | {/* {card.error &&

{card.error}

} */} 69 | 70 |
71 | 72 | 73 |
74 |
75 |
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 |
22 | 23 | dispatch(setAccessKeyID(e.target.value))}/> 24 |
25 | 26 | dispatch(setSecretAccessKey(e.target.value))}/> 27 | 28 | 29 |
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 | --------------------------------------------------------------------------------