├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── E2E │ └── Home.cy.ts ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── server ├── controllers │ ├── customQueryController.d.ts │ ├── customQueryController.js │ ├── customQueryController.ts │ ├── dbConnectionController.d.ts │ ├── dbConnectionController.js │ ├── dbConnectionController.ts │ ├── dbERDcontroller.d.ts │ ├── dbERDcontroller.js │ ├── dbERDcontroller.ts │ ├── dbGenericQueryTesting.d.ts │ ├── dbGenericQueryTesting.js │ ├── dbGenericQueryTesting.ts │ ├── dbHistoryController.d.ts │ ├── dbHistoryController.js │ ├── dbHistoryController.ts │ ├── dbInfoController.d.ts │ ├── dbInfoController.js │ ├── dbInfoController.ts │ ├── dbOverviewConroller.d.ts │ ├── dbOverviewConroller.js │ ├── dbOverviewConroller.ts │ ├── genericMetricsController.d.ts │ ├── genericMetricsController.js │ └── genericMetricsController.ts ├── routes │ ├── pgRoute.d.ts │ ├── pgRoute.js │ └── pgRoute.ts ├── server.d.ts ├── server.js └── server.ts ├── src ├── App.css ├── App.tsx ├── Layout.tsx ├── Types.ts ├── assets │ ├── GIFs │ │ ├── CustomQuery_gif.gif │ │ ├── Dashboard_gif.gif │ │ └── ERD_gif.gif │ ├── LogoIcon.png │ ├── Print_Transparent-cropped.svg │ ├── devPhotos │ │ └── Kurt.jpeg │ ├── fk_icon.png │ ├── logo-horizontal-v2-darkmode.png │ ├── logo-horizontal-v2.png │ ├── logov1.png │ ├── logov1.svg │ ├── pk_icon.png │ ├── react.svg │ ├── screenshots │ │ ├── screenshot1.jpg │ │ ├── screenshot2.jpg │ │ └── screenshot3.jpg │ ├── team_headshots │ │ ├── dkim_headshot.jpg │ │ ├── dmurcia_headshot.jpg │ │ ├── kbulau_headshot.png │ │ ├── other_dkim.jpg │ │ ├── sheck_headshot.jpg │ │ └── ytalab_headshot.jpg │ └── techstack_icons │ │ ├── expressjslogo.png │ │ ├── jestlogo.png │ │ ├── nodelogo.png │ │ ├── postgresqllogo.png │ │ ├── reactlogo.png │ │ ├── tailwindlogo.png │ │ ├── typescriptlogo.png │ │ └── vitelogo.png ├── components │ ├── Errors │ │ ├── AppFallback.tsx │ │ └── RouteError.tsx │ ├── ReactFlow │ │ ├── RFTable.tsx │ │ ├── createEdges.ts │ │ ├── createNodes.ts │ │ └── flow.tsx │ ├── charts │ │ ├── ColumnIndexSizes.tsx │ │ ├── DbSizeCards.tsx │ │ ├── GeneralMetrics.tsx │ │ ├── IndexPerTable.tsx │ │ ├── PolarChart.tsx │ │ ├── QueryTimes.tsx │ │ ├── RowsPerTable.tsx │ │ ├── SlowestCommonQueriesTop10.tsx │ │ ├── SlowestQueriesTop10.tsx │ │ ├── TableIndexSizes.tsx │ │ ├── TableSize.tsx │ │ └── execTimeByOperation.tsx │ ├── customQueryCharts │ │ ├── CustomQueryGeneralMetrics.tsx │ │ ├── MeanPlanningExecutionTimes.tsx │ │ └── PlanningExecutionTimes.tsx │ ├── layout │ │ ├── ConnectDB.tsx │ │ ├── CustomQueryView.tsx │ │ ├── DashNav.tsx │ │ ├── ERDView.tsx │ │ ├── Footer.tsx │ │ ├── MetricsView.tsx │ │ └── NavBar.tsx │ └── ui │ │ ├── CustomQueryBox.tsx │ │ ├── DarkModeToggle.tsx │ │ ├── DashboardCard.tsx │ │ ├── DropdownMenu.tsx │ │ ├── HelpModal.tsx │ │ ├── HomeDropdownMenu.tsx │ │ ├── Loading.tsx │ │ ├── LoginButton.tsx │ │ ├── LogoutButton.tsx │ │ ├── MetricsSeparator.tsx │ │ ├── Modal.tsx │ │ ├── QueryForm.tsx │ │ └── TechStackBar.tsx ├── index.css ├── main.tsx ├── pages │ ├── About.tsx │ ├── Dashboard.tsx │ ├── Home.tsx │ └── Login.tsx ├── store │ ├── appStore.ts │ └── flowStore.ts ├── test │ ├── App.test.tsx │ ├── CustomQuery.test.tsx │ ├── Dashboard.test.tsx │ ├── ERDView.test.tsx │ ├── RFSetup.ts │ └── setup.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.node.tsbuildinfo ├── vite readme.md └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { "node": true, browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | "plugin:react/recommended" 9 | ], 10 | "parserOptions": { 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | ignorePatterns: ['dist', '.eslintrc.cjs'], 17 | parser: '@typescript-eslint/parser', 18 | plugins: ['react-refresh'], 19 | rules: { 20 | 'react-refresh/only-export-components': [ 21 | 'warn', 22 | { allowConstantExport: true }, 23 | ], 24 | "indent": ["warn", 2], 25 | "no-unused-vars": ["off", { "vars": "local" }], 26 | "prefer-const": "warn", 27 | "quotes": ["warn", "single"], 28 | "react/prop-types": "off", 29 | "semi": ["warn", "always"], 30 | "space-infix-ops": "warn" 31 | }, 32 | "settings": { 33 | "react": { "version": "detect"} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.17.1 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app/ 4 | RUN npm install 5 | RUN npm run build 6 | RUN npm run build:server 7 | EXPOSE 3000 8 | ENTRYPOINT ["node", "./server/server.js"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs 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 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(_on, _config) { 6 | // implement node event listeners here 7 | }, 8 | baseUrl: 'http://localhost:5173' 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/E2E/Home.cy.ts: -------------------------------------------------------------------------------- 1 | describe("", ()=> { 2 | before("Render webpage", ()=> { 3 | cy.visit("/") 4 | }) 5 | it("Should have about component", () => { 6 | cy.get("div").should("have.text", "About") 7 | }) 8 | }) -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ScanQL 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scanql", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:server\"", 8 | "dev:frontend": "vite", 9 | "dev:server": "nodemon --watch './**/*.ts' --exec ts-node-esm ./server/server.ts", 10 | "build": "tsc && vite build", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "preview": "vite preview", 13 | "build:server": "tsc -p tsconfig.node.json", 14 | "start": "nodemon --watch './**/*.ts' --exec ts-node-esm ./server/server.ts", 15 | "test": "vitest", 16 | "cypress:open": "cypress open" 17 | }, 18 | "dependencies": { 19 | "@auth0/auth0-react": "^2.2.0", 20 | "@radix-ui/react-dialog": "^1.0.4", 21 | "@radix-ui/react-dropdown-menu": "^2.0.5", 22 | "@radix-ui/react-form": "^0.0.3", 23 | "@radix-ui/react-icons": "^1.3.0", 24 | "@radix-ui/react-progress": "^1.0.3", 25 | "@radix-ui/react-separator": "^1.0.3", 26 | "@radix-ui/react-switch": "^1.0.3", 27 | "@radix-ui/themes": "^1.0.0", 28 | "@tisoap/react-flow-smart-edge": "^3.0.0", 29 | "@types/express": "^4.17.17", 30 | "chart.js": "^4.3.3", 31 | "cypress": "^13.1.0", 32 | "esbuild": "^0.19.2", 33 | "express": "^4.18.2", 34 | "flow": "^0.2.3", 35 | "nodemon": "^3.0.1", 36 | "pg": "^8.11.3", 37 | "react": "^18.2.0", 38 | "react-chartjs-2": "^5.2.0", 39 | "react-dom": "^18.2.0", 40 | "react-error-boundary": "^4.0.11", 41 | "react-responsive": "^9.0.2", 42 | "react-router-dom": "^6.15.0", 43 | "reactflow": "^11.8.2", 44 | "zustand": "^4.4.1" 45 | }, 46 | "devDependencies": { 47 | "@testing-library/jest-dom": "^6.1.2", 48 | "@testing-library/react": "^14.0.0", 49 | "@testing-library/user-event": "^14.4.3", 50 | "@types/node": "^20.5.0", 51 | "@types/pg": "^8.10.2", 52 | "@types/react": "^18.2.15", 53 | "@types/react-dom": "^18.2.7", 54 | "@typescript-eslint/eslint-plugin": "^6.0.0", 55 | "@typescript-eslint/parser": "^6.0.0", 56 | "@vitejs/plugin-react-swc": "^3.3.2", 57 | "@vitest/coverage-v8": "^0.34.3", 58 | "autoprefixer": "^10.4.15", 59 | "concurrently": "^8.2.1", 60 | "eslint": "^8.45.0", 61 | "eslint-plugin-react-hooks": "^4.6.0", 62 | "eslint-plugin-react-refresh": "^0.4.3", 63 | "jsdom": "^22.1.0", 64 | "postcss": "^8.4.28", 65 | "prettier": "3.0.2", 66 | "tailwindcss": "^3.3.3", 67 | "ts-node": "^10.9.1", 68 | "typescript": "^5.0.2", 69 | "vite": "^4.4.5", 70 | "vitest": "^0.34.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/controllers/customQueryController.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type CustomDBController = { 3 | customQueryMetrics: RequestHandler; 4 | }; 5 | declare const customDBController: CustomDBController; 6 | export default customDBController; 7 | -------------------------------------------------------------------------------- /server/controllers/dbConnectionController.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type DbConnectionController = { 3 | connectAndInitializeDB: RequestHandler; 4 | createExtension: RequestHandler; 5 | checkUserPermissions: RequestHandler; 6 | }; 7 | declare const dbConnectionController: DbConnectionController; 8 | export default dbConnectionController; 9 | -------------------------------------------------------------------------------- /server/controllers/dbConnectionController.js: -------------------------------------------------------------------------------- 1 | import pkg from 'pg'; 2 | const { Pool } = pkg; 3 | // on request, connect to user's database and return query pool on res.locals 4 | const dbConnectionController = { 5 | //create controller for first time connection and storage 6 | connectAndInitializeDB: async (req, res, next) => { 7 | // Connecting to database by first retrieve the uri sent from the client then initializing the Pool instance labled pool. Lastly declating db to use the query method from pool and passing array to use in query 8 | const uri_string = req.body.uri; 9 | const pool = new Pool({ 10 | connectionString: uri_string, 11 | }); 12 | const db = { 13 | query: (text, params) => { 14 | return pool.query(text, params); 15 | }, 16 | explainQuery: (text, params) => { 17 | return pool.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true, FORMAT JSON) ${text}`, params); 18 | }, 19 | }; 20 | // Sam added try catch block because there was no error being caught on the server when an invalid URI was entered - it was causing the server to crash. 21 | try { 22 | await db.query('SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';'); 23 | } 24 | catch (err) { 25 | return next({ 26 | log: 'URI invalid, could not connect to database', 27 | status: 400, 28 | message: { 29 | error: `URI invalid, could not connect to database: ${err}`, 30 | } 31 | }); 32 | } 33 | res.locals.dbConnection = db; 34 | res.locals.result = {}; 35 | const queryString = 'SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';'; 36 | try { 37 | // const dbStats = 38 | await db.query(queryString); 39 | res.locals.result.validURI = true; 40 | res.locals.result.currentStats = 'Connected To Database'; 41 | return next(); 42 | } 43 | catch (error) { 44 | return next({ 45 | log: `ERROR caught in connectController.connectAndInitializeDB: ${error}`, 46 | message: { 47 | Error: `Error: ${error}`, 48 | } 49 | }); 50 | } 51 | }, 52 | // creates pg_stat_statements if not already created 53 | // createExtension: async (req, res, next) => { 54 | // const db = res.locals.dbConnection; 55 | // const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'; 56 | // try { 57 | // await db.query(queryString); 58 | // res.locals.result.validURI = true; 59 | // return next(); 60 | // } catch (error) { 61 | // return next({ 62 | // log: `ERROR caught in connectController.createExtension: ${error}`, 63 | // status: 400, 64 | // message: 65 | // 'ERROR: error has occured in connectController.createExtension', 66 | // }); 67 | // } 68 | // }, 69 | // initializes pg_stat_statements if not already initialized 70 | // first controller to stop response cycle and return an error if connection fails 71 | createExtension: async (_req, res, next) => { 72 | const db = res.locals.dbConnection; 73 | const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'; 74 | try { 75 | await db.query(queryString); 76 | res.locals.result.validURI = true; 77 | return next(); 78 | } 79 | catch (error) { 80 | return next({ 81 | log: `ERROR caught in connectController.createExtension: ${error}`, 82 | status: 400, 83 | message: 'ERROR: error has occured in connectController.createExtension', 84 | }); 85 | } 86 | }, 87 | checkUserPermissions: async (req, res, next) => { 88 | const db = res.locals.dbConnection; 89 | const username = req.body.username; // Assume the username is passed in the request body 90 | const dbname = req.body.dbname; // Assume the dbname is passed in the request body 91 | const queryString = ` 92 | SELECT 93 | has_database_privilege('${username}', '${dbname}', 'CONNECT') AS can_connect 94 | `; 95 | //checking if user can connect to the e 96 | const permissionsQuery = ` 97 | SELECT table_schema, table_name, privilege_type 98 | FROM information_schema.role_table_grants 99 | WHERE grantee = $1; 100 | `; 101 | try { 102 | const result = await db.query(queryString); // the result of whether the user can connect or not 103 | const canConnect = result.rows[0]?.can_connect || false; 104 | const permissions = await db.query(permissionsQuery, [username]); //the permissions of that user 105 | const userPermissions = permissions.rows; 106 | res.locals.userPermissions = userPermissions; 107 | if (canConnect) { 108 | return next(); // User has CONNECT privilege, so continue to the next middleware 109 | } 110 | else { 111 | return next({ 112 | log: 'ERROR: User does not have CONNECT privilege on this database', 113 | status: 403, 114 | message: 'ERROR: User does not have necessary permissions', 115 | }); 116 | } 117 | } 118 | catch (error) { 119 | return next({ 120 | log: `ERROR caught in connectController.checkUserPermissions: ${error}`, 121 | status: 500, 122 | message: 'ERROR: error has occurred while checking user permissions', 123 | }); 124 | } 125 | }, 126 | }; 127 | export default dbConnectionController; 128 | -------------------------------------------------------------------------------- /server/controllers/dbConnectionController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import pkg from 'pg'; 3 | const { Pool } = pkg; 4 | 5 | type DbConnectionController = { 6 | connectAndInitializeDB: RequestHandler; 7 | createExtension: RequestHandler; 8 | checkUserPermissions: RequestHandler; 9 | }; 10 | 11 | // on request, connect to user's database and return query pool on res.locals 12 | const dbConnectionController: DbConnectionController = { 13 | //create controller for first time connection and storage 14 | 15 | 16 | connectAndInitializeDB: async (req, res, next) => { 17 | // Connecting to database by first retrieve the uri sent from the client then initializing the Pool instance labled pool. Lastly declating db to use the query method from pool and passing array to use in query 18 | const uri_string = req.body.uri; 19 | const pool = new Pool({ 20 | connectionString: uri_string, 21 | }); 22 | 23 | const db = { 24 | query: (text: string, params?: Array) => { 25 | return pool.query(text, params); 26 | }, 27 | explainQuery: (text: string, params?: Array) => { 28 | return pool.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true, FORMAT JSON) ${text}`, params); 29 | }, 30 | }; 31 | // Sam added try catch block because there was no error being caught on the server when an invalid URI was entered - it was causing the server to crash. 32 | try { 33 | await db.query('SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';'); 34 | } catch (err) { 35 | return next({ 36 | log: 'URI invalid, could not connect to database', 37 | status: 400, 38 | message: { 39 | error: `URI invalid, could not connect to database: ${err}`, 40 | } 41 | }); 42 | } 43 | res.locals.dbConnection = db; 44 | res.locals.result = {}; 45 | 46 | const queryString = 'SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';'; 47 | 48 | try { 49 | // const dbStats = 50 | await db.query(queryString); 51 | res.locals.result.validURI = true; 52 | res.locals.result.currentStats = 'Connected To Database'; 53 | return next(); 54 | } catch (error) { 55 | return next({ 56 | log: `ERROR caught in connectController.connectAndInitializeDB: ${error}`, 57 | message: { 58 | Error: `Error: ${error}`, 59 | } 60 | }); 61 | } 62 | }, 63 | 64 | 65 | // creates pg_stat_statements if not already created 66 | 67 | // createExtension: async (req, res, next) => { 68 | // const db = res.locals.dbConnection; 69 | // const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'; 70 | // try { 71 | // await db.query(queryString); 72 | // res.locals.result.validURI = true; 73 | // return next(); 74 | // } catch (error) { 75 | // return next({ 76 | // log: `ERROR caught in connectController.createExtension: ${error}`, 77 | // status: 400, 78 | // message: 79 | // 'ERROR: error has occured in connectController.createExtension', 80 | // }); 81 | // } 82 | // }, 83 | // initializes pg_stat_statements if not already initialized 84 | // first controller to stop response cycle and return an error if connection fails 85 | createExtension: async (_req, res, next) => { 86 | const db = res.locals.dbConnection; 87 | const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'; 88 | try { 89 | await db.query(queryString); 90 | res.locals.result.validURI = true; 91 | return next(); 92 | } catch (error) { 93 | return next({ 94 | log: `ERROR caught in connectController.createExtension: ${error}`, 95 | status: 400, 96 | message: 97 | 'ERROR: error has occured in connectController.createExtension', 98 | }); 99 | } 100 | }, 101 | checkUserPermissions: async (req, res, next) => { 102 | const db = res.locals.dbConnection; 103 | const username = req.body.username; // Assume the username is passed in the request body 104 | const dbname = req.body.dbname; // Assume the dbname is passed in the request body 105 | 106 | const queryString = ` 107 | SELECT 108 | has_database_privilege('${username}', '${dbname}', 'CONNECT') AS can_connect 109 | `; 110 | //checking if user can connect to the e 111 | const permissionsQuery = ` 112 | SELECT table_schema, table_name, privilege_type 113 | FROM information_schema.role_table_grants 114 | WHERE grantee = $1; 115 | `; 116 | try { 117 | const result = await db.query(queryString); // the result of whether the user can connect or not 118 | const canConnect = result.rows[0]?.can_connect || false; 119 | const permissions = await db.query(permissionsQuery, [username]);//the permissions of that user 120 | const userPermissions = permissions.rows 121 | res.locals.userPermissions = userPermissions 122 | if (canConnect) { 123 | return next(); // User has CONNECT privilege, so continue to the next middleware 124 | } else { 125 | return next({ 126 | log: 'ERROR: User does not have CONNECT privilege on this database', 127 | status: 403, // HTTP 403 Forbidden 128 | message: 'ERROR: User does not have necessary permissions', 129 | }); 130 | } 131 | } catch (error) { 132 | return next({ 133 | log: `ERROR caught in connectController.checkUserPermissions: ${error}`, 134 | status: 500, 135 | message: 'ERROR: error has occurred while checking user permissions', 136 | }); 137 | } 138 | }, 139 | }; 140 | 141 | export default dbConnectionController; -------------------------------------------------------------------------------- /server/controllers/dbERDcontroller.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | interface schemaControllers { 3 | getSchemaPostgreSQL: RequestHandler; 4 | } 5 | declare const dbERDcontroller: schemaControllers; 6 | export default dbERDcontroller; 7 | -------------------------------------------------------------------------------- /server/controllers/dbERDcontroller.js: -------------------------------------------------------------------------------- 1 | // 2 | const dbERDcontroller = { 3 | getSchemaPostgreSQL: async (_req, res, next) => { 4 | try { 5 | const pg = res.locals.dbConnection; 6 | // Get all relationships between all tables 7 | // Identify the current schema name for use in full schema query 8 | const currentSchemaSQL = await pg.query(`SELECT current_schema FROM current_schema`); 9 | const currentSchema = currentSchemaSQL.rows[0].current_schema; 10 | const constraintArr = []; 11 | // Get Relationships, Tables names, Column names, Data types 12 | const query = `SELECT * FROM ( 13 | SELECT DISTINCT ON (c.table_name, c.column_name) 14 | c.table_name, 15 | c.column_name, 16 | c.data_type, 17 | c. ordinal_position, 18 | c.is_nullable, 19 | max(case when tc.constraint_type = 'PRIMARY KEY' then 1 else 0 end) OVER(PARTITION BY c.table_name, c.column_name) AS is_primary_key, 20 | max(case when tc.constraint_type = 'FOREIGN KEY' then 1 else 0 end) OVER(PARTITION BY c.table_name, c.column_name) AS is_foreign_key, 21 | cc.table_name as table_origin, 22 | cc.column_name as table_column 23 | 24 | FROM information_schema.key_column_usage kc 25 | 26 | INNER JOIN information_schema.table_constraints tc 27 | ON kc.table_name = tc.table_name AND kc.table_schema = tc.table_schema AND kc.constraint_name = tc.constraint_name 28 | 29 | LEFT JOIN information_schema.constraint_column_usage cc 30 | ON cc.constraint_name = kc.constraint_name AND tc.constraint_type = 'FOREIGN KEY' 31 | 32 | RIGHT JOIN information_schema.columns c 33 | ON c.table_name = kc.table_name AND c.column_name = kc.column_name 34 | 35 | WHERE c.table_schema = $1 AND is_updatable = 'YES' 36 | 37 | ORDER BY c.table_name, c.column_name, is_primary_key desc, table_origin) subquery 38 | 39 | ORDER BY table_name, ordinal_position;`; 40 | const schema = await pg.query(query, [currentSchema]); 41 | // Initialize array to hold returned data 42 | const erDiagram = {}; 43 | let tableObj = {}; 44 | // Make custom type for any on tableObj 45 | // Iterate through array of all table names, columns, and data types 46 | for (let i = 0; i < schema.rows.length; i++) { 47 | let nextTableName; 48 | if (schema.rows[i + 1]) 49 | nextTableName = schema.rows[i + 1].table_name; 50 | // current represents each object in the array 51 | const current = schema.rows[i]; 52 | //column object type and declaration 53 | // Assign table name and column name 54 | tableObj[current.column_name] = {}; 55 | tableObj[current.column_name].table_name = current.table_name; 56 | tableObj[current.column_name].column_name = current.column_name; 57 | // Assign data type 58 | if (current.data_type === 'integer') 59 | tableObj[current.column_name].data_type = 'int'; 60 | else if (current.data_type === 'character varying') 61 | tableObj[current.column_name].data_type = 'varchar'; 62 | else if (current.data_type === 'timestamp without time zone') 63 | tableObj[current.column_name].data_type = 'date'; 64 | else 65 | tableObj[current.column_name].data_type = current.data_type; 66 | // Add relationships and constraints if there are any 67 | if (current.is_primary_key) { 68 | tableObj[current.column_name].primary_key = 'true'; 69 | tableObj[current.column_name].foreign_tables = []; 70 | } 71 | else if (!current.is_primary_key) { 72 | tableObj[current.column_name].primary_key = 'false'; 73 | } 74 | // Add foreign keys 75 | if (current.is_foreign_key) { 76 | tableObj[current.column_name].foreign_key = 'true'; 77 | } 78 | else if (!current.is_foreign_key) { 79 | tableObj[current.column_name].foreign_key = 'false'; 80 | } 81 | // Add NOT NULL to ERD 82 | if (current.is_nullable === 'NO') { 83 | tableObj[current.column_name].Constraints = "NOT NULL"; 84 | } 85 | // table_origin is only given when column is a foreign key 86 | if (current.table_origin) { 87 | const constraintObj = {}; 88 | constraintObj[`${[current.table_origin]}.${current.table_column}`] = current.table_name; 89 | tableObj[current.column_name].linkedTable = current.table_origin; 90 | tableObj[current.column_name].linkedTableColumn = 91 | current.table_column; 92 | constraintArr.push({ ...constraintObj }); 93 | } 94 | // if table name at next row is a different table, 95 | // push a deep copy of the tableObj to final ER diagram and reset tableObj 96 | if (!nextTableName || nextTableName !== current.table_name) { 97 | erDiagram[current.table_name] = { ...tableObj }; 98 | tableObj = {}; 99 | } 100 | } 101 | for (const constraint of constraintArr) { 102 | for (const relationship in constraint) { 103 | const string = relationship.split('.'); // [species, _id] 104 | const tableName = string[0]; // species 105 | const columnName = string[1]; // _id 106 | const tableOrigin = constraint[relationship]; // people 107 | erDiagram[tableName][columnName].foreign_tables.push(tableOrigin); 108 | } 109 | } 110 | res.locals.erDiagram = erDiagram; 111 | return next(); 112 | } 113 | catch (error) { 114 | return next({ 115 | log: `Error in schemaController.getSchemaPostgreSQL ${error}`, 116 | status: 400, 117 | message: { error }, 118 | }); 119 | } 120 | }, 121 | }; 122 | export default dbERDcontroller; 123 | -------------------------------------------------------------------------------- /server/controllers/dbERDcontroller.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | // import dotenv from 'dotenv'; 3 | // import db from '../models/userModel'; 4 | // import { table } from 'console'; 5 | // import Cryptr from 'cryptr'; 6 | // dotenv.config(); 7 | 8 | interface schemaControllers { 9 | getSchemaPostgreSQL: RequestHandler; 10 | } 11 | // 12 | const dbERDcontroller: schemaControllers = { 13 | getSchemaPostgreSQL: async (_req, res, next) => { 14 | try { 15 | const pg = res.locals.dbConnection; 16 | // Get all relationships between all tables 17 | // Identify the current schema name for use in full schema query 18 | const currentSchemaSQL = await pg.query( 19 | `SELECT current_schema FROM current_schema` 20 | ); 21 | 22 | const currentSchema = currentSchemaSQL.rows[0].current_schema; 23 | 24 | const constraintArr: Record[] = []; 25 | // Get Relationships, Tables names, Column names, Data types 26 | const query = `SELECT * FROM ( 27 | SELECT DISTINCT ON (c.table_name, c.column_name) 28 | c.table_name, 29 | c.column_name, 30 | c.data_type, 31 | c. ordinal_position, 32 | c.is_nullable, 33 | max(case when tc.constraint_type = 'PRIMARY KEY' then 1 else 0 end) OVER(PARTITION BY c.table_name, c.column_name) AS is_primary_key, 34 | max(case when tc.constraint_type = 'FOREIGN KEY' then 1 else 0 end) OVER(PARTITION BY c.table_name, c.column_name) AS is_foreign_key, 35 | cc.table_name as table_origin, 36 | cc.column_name as table_column 37 | 38 | FROM information_schema.key_column_usage kc 39 | 40 | INNER JOIN information_schema.table_constraints tc 41 | ON kc.table_name = tc.table_name AND kc.table_schema = tc.table_schema AND kc.constraint_name = tc.constraint_name 42 | 43 | LEFT JOIN information_schema.constraint_column_usage cc 44 | ON cc.constraint_name = kc.constraint_name AND tc.constraint_type = 'FOREIGN KEY' 45 | 46 | RIGHT JOIN information_schema.columns c 47 | ON c.table_name = kc.table_name AND c.column_name = kc.column_name 48 | 49 | WHERE c.table_schema = $1 AND is_updatable = 'YES' 50 | 51 | ORDER BY c.table_name, c.column_name, is_primary_key desc, table_origin) subquery 52 | 53 | ORDER BY table_name, ordinal_position;`; 54 | const schema = await pg.query(query, [currentSchema]); 55 | 56 | // Initialize array to hold returned data 57 | const erDiagram: Record = {}; 58 | let tableObj: Record = {}; 59 | // Make custom type for any on tableObj 60 | 61 | // Iterate through array of all table names, columns, and data types 62 | for (let i = 0; i < schema.rows.length; i++) { 63 | let nextTableName; 64 | if (schema.rows[i + 1]) nextTableName = schema.rows[i + 1].table_name; 65 | // current represents each object in the array 66 | const current = schema.rows[i]; 67 | //column object type and declaration 68 | 69 | // Assign table name and column name 70 | tableObj[current.column_name] = {}; 71 | tableObj[current.column_name].table_name = current.table_name; 72 | tableObj[current.column_name].column_name = current.column_name; 73 | // Assign data type 74 | if (current.data_type === 'integer') 75 | tableObj[current.column_name].data_type = 'int'; 76 | else if (current.data_type === 'character varying') 77 | tableObj[current.column_name].data_type = 'varchar'; 78 | else if (current.data_type === 'timestamp without time zone') 79 | tableObj[current.column_name].data_type = 'date'; 80 | else tableObj[current.column_name].data_type = current.data_type; 81 | // Add relationships and constraints if there are any 82 | if (current.is_primary_key) { 83 | tableObj[current.column_name].primary_key = 'true'; 84 | tableObj[current.column_name].foreign_tables = []; 85 | } else if (!current.is_primary_key) { 86 | tableObj[current.column_name].primary_key = 'false'; 87 | } 88 | // Add foreign keys 89 | if (current.is_foreign_key) { 90 | tableObj[current.column_name].foreign_key = 'true'; 91 | } else if (!current.is_foreign_key) { 92 | tableObj[current.column_name].foreign_key = 'false'; 93 | } 94 | 95 | // Add NOT NULL to ERD 96 | if (current.is_nullable === 'NO'){ 97 | tableObj[current.column_name].Constraints = "NOT NULL" 98 | } 99 | // table_origin is only given when column is a foreign key 100 | if (current.table_origin) { 101 | const constraintObj: Record = {}; 102 | constraintObj[`${[current.table_origin]}.${current.table_column}`] = current.table_name; 103 | tableObj[current.column_name].linkedTable = current.table_origin; 104 | tableObj[current.column_name].linkedTableColumn = 105 | current.table_column; 106 | 107 | constraintArr.push({ ...constraintObj }); 108 | } 109 | 110 | // if table name at next row is a different table, 111 | // push a deep copy of the tableObj to final ER diagram and reset tableObj 112 | if (!nextTableName || nextTableName !== current.table_name) { 113 | erDiagram[current.table_name] = { ...tableObj }; 114 | tableObj = {}; 115 | } 116 | } 117 | 118 | for (const constraint of constraintArr) { 119 | for (const relationship in constraint) { 120 | const string = relationship.split('.'); // [species, _id] 121 | const tableName = string[0]; // species 122 | const columnName = string[1]; // _id 123 | const tableOrigin = constraint[relationship]; // people 124 | erDiagram[tableName][columnName].foreign_tables.push(tableOrigin); 125 | } 126 | } 127 | 128 | res.locals.erDiagram = erDiagram; 129 | return next(); 130 | } catch (error) { 131 | return next({ 132 | log: `Error in schemaController.getSchemaPostgreSQL ${error}`, 133 | status: 400, 134 | message: { error }, 135 | }); 136 | } 137 | }, 138 | }; 139 | 140 | export default dbERDcontroller; -------------------------------------------------------------------------------- /server/controllers/dbGenericQueryTesting.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type GeneralMetricsController = { 3 | performGenericQueries: RequestHandler; 4 | }; 5 | declare const dbGenericQueryTesting: GeneralMetricsController; 6 | export default dbGenericQueryTesting; 7 | -------------------------------------------------------------------------------- /server/controllers/dbGenericQueryTesting.js: -------------------------------------------------------------------------------- 1 | ///Using for helper functions on delete 2 | // interface ForeignKey { 3 | // column: string; 4 | // referencedTable: string; 5 | // referencedColumn: string; 6 | // } 7 | // type ForeignKeyInfo = {[columName: string]: ForeignKey} 8 | // type PrimaryKeyInfo = { 9 | // [columnName: string]: {datatype: string, isAutoIncrementing: boolean}; 10 | // }; 11 | // interface TableInfo { 12 | // tableName: string; 13 | // numberOfRows: number; 14 | // numberOfIndexes: number; 15 | // numberOfFields: number; 16 | // numberOfForeignKeys: number; 17 | // foreignKeysObj: ForeignKeyInfo 18 | // primaryKeysObj: PrimaryKeyInfo 19 | // } 20 | // interface DBinfo { 21 | // [tablename: string]: TableInfo; 22 | // } 23 | const dbGenericQueryTesting = { 24 | performGenericQueries: async (req, res, next) => { 25 | const db = res.locals.dbConnection; 26 | const tableNames = res.locals.tableNames; 27 | const dbInfo = res.locals.databaseInfo; 28 | const executionPlans = {}; 29 | // await db.query('BEGIN'); // Start the transaction 30 | try { 31 | for (const tableName of tableNames) { 32 | const tableInfo = dbInfo[tableName]; 33 | const primaryKeysObject = tableInfo.primaryKeysObj; 34 | const checkContraintObj = tableInfo.checkConstraints; 35 | const foreignKeysObj = tableInfo.foreignKeysObj; 36 | const sampleData = tableInfo.sampleData; 37 | const columnDataTypes = tableInfo.columnDataTypes; 38 | //UPDATe Test 39 | //we want to update the sample row by changing only the non constrained column values 40 | let updateColumn; 41 | let updateValue; 42 | for (const col of sampleData) { 43 | if (!primaryKeysObject[col] && !foreignKeysObj[col] && !checkContraintObj[col]) { 44 | updateColumn = col; 45 | updateValue = sampleData[col]; 46 | break; 47 | } 48 | } 49 | const updateQuery = `UPDATE ${updateValue} SET ${updateColumn} = $1 WHERE ${updateColumn} = ${sampleData[updateColumn]}`; 50 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, /* unchangedSample[pkArray[pkArray.length - 1]]*/]); 51 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 52 | //done with update 53 | } 54 | } 55 | catch (error) { 56 | //Rollback if an error is caught 57 | // await db.query('ROLLBACK'); 58 | return next({ 59 | log: `ERROR caught in generalMetricsController.performGenericQueries: ${error}`, 60 | status: 400, 61 | message: 'ERROR: error has occurred in generalMetricsController.performGenericQueries', 62 | }); 63 | } 64 | } 65 | }; 66 | export default dbGenericQueryTesting; 67 | -------------------------------------------------------------------------------- /server/controllers/dbGenericQueryTesting.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler, query } from 'express'; 2 | // import { explainQuery } from '../helpers/explainQuery'; 3 | import { QueryResult } from 'pg'; 4 | 5 | type GeneralMetricsController = { 6 | performGenericQueries: RequestHandler; 7 | }; 8 | type QueryResults = { 9 | query?: string; 10 | plan?: QueryResult; 11 | values?: any; 12 | otherExecutions?: TableResults 13 | }; 14 | 15 | type TableResults = { 16 | [tablename: string]: QueryResults 17 | }; 18 | 19 | type ExecutionPlans = { 20 | [tablename: string]: TableResults; 21 | }; 22 | ///Using for helper functions on delete 23 | // interface ForeignKey { 24 | // column: string; 25 | // referencedTable: string; 26 | // referencedColumn: string; 27 | // } 28 | // type ForeignKeyInfo = {[columName: string]: ForeignKey} 29 | // type PrimaryKeyInfo = { 30 | // [columnName: string]: {datatype: string, isAutoIncrementing: boolean}; 31 | // }; 32 | // interface TableInfo { 33 | // tableName: string; 34 | // numberOfRows: number; 35 | // numberOfIndexes: number; 36 | // numberOfFields: number; 37 | // numberOfForeignKeys: number; 38 | // foreignKeysObj: ForeignKeyInfo 39 | // primaryKeysObj: PrimaryKeyInfo 40 | // } 41 | // interface DBinfo { 42 | // [tablename: string]: TableInfo; 43 | // } 44 | 45 | const dbGenericQueryTesting: GeneralMetricsController = { 46 | performGenericQueries: async (req, res, next) => { 47 | const db = res.locals.dbConnection; 48 | 49 | const tableNames = res.locals.tableNames; 50 | 51 | const dbInfo = res.locals.databaseInfo; 52 | 53 | const executionPlans: ExecutionPlans = {}; 54 | 55 | // await db.query('BEGIN'); // Start the transaction 56 | try{ 57 | for (const tableName of tableNames) { 58 | const tableInfo = dbInfo[tableName]; 59 | const primaryKeysObject = tableInfo.primaryKeysObj; 60 | const checkContraintObj = tableInfo.checkConstraints; 61 | const foreignKeysObj = tableInfo.foreignKeysObj; 62 | const sampleData = tableInfo.sampleData; 63 | const columnDataTypes = tableInfo.columnDataTypes; 64 | //UPDATe Test 65 | //we want to update the sample row by changing only the non constrained column values 66 | let updateColumn; 67 | let updateValue; 68 | for(const col of sampleData){ 69 | if(!primaryKeysObject[col] && !foreignKeysObj[col] && !checkContraintObj[col]){ 70 | updateColumn = col; 71 | updateValue = sampleData[col]; 72 | break; 73 | } 74 | } 75 | const updateQuery = `UPDATE ${updateValue} SET ${updateColumn} = $1 WHERE ${updateColumn} = ${sampleData[updateColumn]}`; 76 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, /* unchangedSample[pkArray[pkArray.length - 1]]*/]); 77 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 78 | //done with update 79 | } 80 | } 81 | catch(error) { 82 | //Rollback if an error is caught 83 | // await db.query('ROLLBACK'); 84 | return next({ 85 | log: `ERROR caught in generalMetricsController.performGenericQueries: ${error}`, 86 | status: 400, 87 | message: 'ERROR: error has occurred in generalMetricsController.performGenericQueries', 88 | }); 89 | } 90 | } 91 | }; 92 | 93 | export default dbGenericQueryTesting; -------------------------------------------------------------------------------- /server/controllers/dbHistoryController.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type DBHistoryController = { 3 | dbPastMetrics: RequestHandler; 4 | }; 5 | declare const dBHistoryController: DBHistoryController; 6 | export default dBHistoryController; 7 | -------------------------------------------------------------------------------- /server/controllers/dbHistoryController.js: -------------------------------------------------------------------------------- 1 | const dBHistoryController = { 2 | dbPastMetrics: async (_req, res, next) => { 3 | try { 4 | const db = res.locals.dbConnection; 5 | const slowestTotalQueriesString = await db.query(` 6 | SELECT 7 | query, 8 | CASE 9 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'SELECT' THEN 'SELECT' 10 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'UPDATE' THEN 'UPDATE' 11 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'INSERT' THEN 'INSERT' 12 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'DELETE' THEN 'DELETE' 13 | ELSE 'OTHER' 14 | END AS operation, 15 | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_exec_time) AS median_exec_time, 16 | AVG(mean_exec_time) AS mean_exec_time, 17 | STDDEV(total_exec_time) AS stdev_exec_time, 18 | MIN(total_exec_time) AS min_exec_time, 19 | MAX(total_exec_time) AS max_exec_time 20 | FROM 21 | pg_stat_statements 22 | GROUP BY 23 | query 24 | ORDER BY 25 | mean_exec_time DESC 26 | LIMIT 10; `); 27 | const slowestTotalQueriesResults = [...slowestTotalQueriesString.rows]; //copy of array 28 | const totalQueries = {}; 29 | slowestTotalQueriesResults.forEach((slowQuery, index) => { 30 | if (slowQuery.operation !== 'OTHER') { 31 | totalQueries[`${slowQuery.operation} Query ${index + 1}`] = { 32 | query: slowQuery.query, 33 | median: slowQuery.median_exec_time, 34 | mean: slowQuery.mean_exec_time, 35 | }; 36 | } 37 | }); 38 | // res.locals.totalQueries = totalQueries; 39 | const slowestCommonQueriesString = await db.query(` 40 | SELECT 41 | query, 42 | CASE 43 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'SELECT' THEN 'SELECT' 44 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'UPDATE' THEN 'UPDATE' 45 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'INSERT' THEN 'INSERT' 46 | WHEN UPPER(LEFT(TRIM(query), 6)) = 'DELETE' THEN 'DELETE' 47 | ELSE 'OTHER' 48 | END AS operation, 49 | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_exec_time) AS median_exec_time, 50 | AVG(mean_exec_time) AS mean_exec_time, 51 | MIN(total_exec_time) AS min_exec_time, 52 | MAX(total_exec_time) AS max_exec_time, 53 | COUNT(*) AS execution_count 54 | FROM 55 | pg_stat_statements 56 | GROUP BY 57 | query 58 | ORDER BY 59 | execution_count DESC 60 | LIMIT 10;`); 61 | const slowestCommonQueriesResults = [...slowestCommonQueriesString.rows]; //copy of array 62 | const commonQueries = {}; 63 | slowestCommonQueriesResults.forEach((slowQuery, index) => { 64 | if (slowQuery.operation !== 'OTHER') { 65 | commonQueries[`${slowQuery.operation} Query ${index + 1}`] = { 66 | query: slowQuery.query, 67 | median: slowQuery.median_exec_time, 68 | mean: slowQuery.mean_exec_time, 69 | count: slowQuery.execution_count, 70 | }; 71 | } 72 | }); 73 | // res.locals.slowestCommonQueriesString = commonQueries; 74 | const overAllQueryAggregatesString = await db.query(` 75 | WITH operations_cte AS ( 76 | SELECT unnest(ARRAY['SELECT', 'UPDATE', 'INSERT', 'DELETE']) AS operation 77 | ) 78 | SELECT 79 | operations_cte.operation, 80 | COALESCE(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_exec_time), 0) AS median_exec_time, 81 | COALESCE(AVG(mean_exec_time), 0) AS mean_exec_time, 82 | COALESCE(MIN(total_exec_time), 0) AS min_exec_time, 83 | COALESCE(MAX(total_exec_time), 0) AS max_exec_time, 84 | COALESCE(SUM(CASE 85 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'SELECT' THEN 1 86 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'UPDATE' THEN 1 87 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'INSERT' THEN 1 88 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'DELETE' THEN 1 89 | ELSE 0 90 | END), 0) AS execution_count 91 | FROM 92 | operations_cte 93 | LEFT JOIN 94 | pg_stat_statements ON operations_cte.operation = 95 | CASE 96 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'SELECT' THEN 'SELECT' 97 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'UPDATE' THEN 'UPDATE' 98 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'INSERT' THEN 'INSERT' 99 | WHEN UPPER(LEFT(TRIM(pg_stat_statements.query), 6)) = 'DELETE' THEN 'DELETE' 100 | ELSE 'OTHER' 101 | END 102 | GROUP BY 103 | operations_cte.operation 104 | ORDER BY 105 | mean_exec_time DESC 106 | LIMIT 10; 107 | `); 108 | //Create NA and 0 for no operation 109 | const operationArr = ['INSERT', 'SELECT', 'UPDATE', 'DELETE']; 110 | const overAllQueryAggregates = {}; 111 | operationArr.forEach((operation) => { 112 | const row = overAllQueryAggregatesString.rows.find((row) => row.operation === operation); 113 | if (row) { 114 | overAllQueryAggregates[operation] = row; 115 | } 116 | else { 117 | overAllQueryAggregates[operation] = { 118 | query: 'N/A', 119 | operation: operation, 120 | median_exec_time: -1, 121 | mean_exec_time: -1, 122 | stdev_exec_time: -1, 123 | min_exec_time: -1, 124 | max_exec_time: -1, 125 | execution_count: 0, 126 | }; 127 | } 128 | }); 129 | // res.locals.overAllQueries = overAllQueryAggregatesString.rows; 130 | // // Building the result object 131 | const dbHistMetrics = { 132 | slowestTotalQueries: totalQueries, 133 | slowestCommonQueries: commonQueries, 134 | execTimesByOperation: overAllQueryAggregates, 135 | }; 136 | res.locals.dbHistMetrics = dbHistMetrics; 137 | return next(); 138 | } 139 | catch (error) { 140 | return next(error); 141 | } 142 | }, 143 | }; 144 | export default dBHistoryController; 145 | -------------------------------------------------------------------------------- /server/controllers/dbInfoController.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type DbInfoController = { 3 | getDataBaseInfo: RequestHandler; 4 | }; 5 | declare const dbInfoController: DbInfoController; 6 | export default dbInfoController; 7 | -------------------------------------------------------------------------------- /server/controllers/dbInfoController.js: -------------------------------------------------------------------------------- 1 | const dbInfoController = { 2 | getDataBaseInfo: async (_req, res, next) => { 3 | // console.log('made it in dbinfo'); 4 | // pulling database connection from res locals 5 | const db = res.locals.dbConnection; 6 | try { 7 | // Retrievie table names 8 | const tables = await db.query('SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';'); 9 | // Check if we have at least one table to work with 10 | if (tables.rows.length === 0) { 11 | res.locals.result = 'No tables found in database.'; 12 | return next(); 13 | } 14 | //array of table names to use in generic metrics function 15 | const tableNames = tables.rows.map(obj => obj.tablename); 16 | // passing tableNames through res locals 17 | res.locals.tableNames = tableNames; 18 | //creating an array of comprehensive info on the tables 19 | const tableInfoPromises = tables.rows.map(async (row) => { 20 | const tableName = row.tablename; 21 | const numberOfFields = await db.query('SELECT COUNT(*) FROM information_schema.columns WHERE table_name = $1;', [tableName]); 22 | const numberOfRows = await db.query(`SELECT COUNT(*) FROM ${tableName};`); 23 | const numberOfIndexes = await db.query('SELECT COUNT(*) FROM pg_indexes WHERE tablename = $1;', [tableName]); 24 | const foreignKeys = await db.query(` 25 | SELECT 26 | kcu.column_name AS column, 27 | ccu.table_name AS referencedTable, 28 | ccu.column_name AS referencedColumn 29 | FROM 30 | information_schema.table_constraints AS tc 31 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name 32 | JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name 33 | WHERE 34 | tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1`, [tableName]); 35 | const foreignKeyObject = {}; 36 | for (const row of foreignKeys.rows) { 37 | foreignKeyObject[row.column] = { 38 | column: row.column, 39 | referencedTable: row.referencedtable, 40 | referencedColumn: row.referencedcolumn 41 | }; 42 | } 43 | const primaryKeys = await db.query(` 44 | SELECT 45 | kcu.column_name AS column, 46 | cols.data_type AS data_type, 47 | CASE 48 | WHEN cols.column_default LIKE 'nextval%' THEN TRUE 49 | ELSE FALSE 50 | END AS is_auto_incrementing 51 | FROM 52 | information_schema.table_constraints AS tc 53 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name 54 | JOIN information_schema.columns AS cols ON tc.table_name = cols.table_name AND kcu.column_name = cols.column_name 55 | WHERE 56 | tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = $1;`, [tableName]); 57 | const primaryKeyObject = {}; 58 | // creating an object of the column name as the key and { datatype, isAutoIncrementing } as the value 59 | for (const row of primaryKeys.rows) { 60 | primaryKeyObject[row.column] = { 61 | datatype: row.data_type, 62 | isAutoIncrementing: row.is_auto_incrementing 63 | }; 64 | } 65 | const sampleData = await db.query(`SELECT * FROM ${tableName} LIMIT 100;`); 66 | const columnDataTypes = await db.query(` 67 | SELECT column_name, data_type, column_default, is_nullable 68 | FROM information_schema.columns 69 | WHERE table_name = $1 AND table_schema = 'public' 70 | ORDER BY ordinal_position;`, [tableName]); 71 | // Now, transform 'result.rows' to the desired format 72 | // const fieldTypes: { [key: string]: string } = {}; 73 | // for (const row of columnDataTypes.rows) { 74 | // fieldTypes[row.column_name] = row.data_type; 75 | // } 76 | // A check constrain is a constraint on a column that requires only specific values be inserted (i.e. "Yes" or "No") 77 | const checkContraints = await db.query(` 78 | SELECT 79 | conname AS constraint_name, 80 | a.attname AS column_name, 81 | con.conkey AS constraint_definition 82 | FROM 83 | pg_constraint con 84 | INNER JOIN 85 | pg_class rel ON rel.oid = con.conrelid 86 | INNER JOIN 87 | pg_attribute a ON a.attnum = ANY(con.conkey) 88 | WHERE 89 | con.contype = 'c' AND rel.relname = $1;`, [tableName]); 90 | const checkContraintObj = {}; 91 | checkContraints.rows.forEach((checkEl) => { 92 | checkContraintObj[checkEl.column_name] = checkEl.constraint_definition; 93 | }); 94 | return { 95 | tableName, 96 | numberOfRows: parseInt(numberOfRows.rows[0].count, 10), 97 | numberOfIndexes: parseInt(numberOfIndexes.rows[0].count, 10), 98 | numberOfFields: parseInt(numberOfFields.rows[0].count, 10), 99 | numberOfForeignKeys: foreignKeys.rowCount, 100 | numberOfPrimaryKeys: primaryKeys.rowCount, 101 | checkConstraints: checkContraintObj, 102 | foreignKeysObj: foreignKeyObject || {}, 103 | primaryKeysObj: primaryKeyObject || {}, 104 | sampleData: sampleData.rows[sampleData.rows.length - 1] || {}, 105 | columnDataTypes: columnDataTypes.rows.map((obj) => { 106 | return ({ column_name: obj.column_name, datatype: obj.data_type }); 107 | }) // Column names and their data types 108 | }; 109 | }); 110 | // Usage 111 | // use Promise.all() to wait for all promises to resolve 112 | // const databaseInfo = await Promise.all(tableInfoPromises); 113 | Promise.all(tableInfoPromises).then((databaseInfo) => { 114 | const databaseInfoMap = {}; 115 | databaseInfo.forEach(info => { 116 | databaseInfoMap[info.tableName] = info; 117 | }); 118 | res.locals.databaseInfo = databaseInfoMap; 119 | return next(); 120 | }); 121 | } 122 | catch (error) { 123 | return next(error); 124 | } 125 | } 126 | }; 127 | export default dbInfoController; 128 | -------------------------------------------------------------------------------- /server/controllers/dbInfoController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | // import pkg from 'pg'; 3 | import { QueryResult } from 'pg'; 4 | // QueryResult doesn't exist in pg package. May need to install another package. 5 | 6 | //PURPOSE: The purpose of this controller is mainly to provide the user a comprehensive birds-eye view of their database. For convience it is also use to supply other controllers with necessary information. 7 | 8 | type DbInfoController = { 9 | getDataBaseInfo: RequestHandler; 10 | }; 11 | 12 | interface ForeignKey { 13 | column: string; 14 | referencedTable: string; 15 | referencedColumn: string; 16 | } 17 | 18 | type ForeignKeyInfo = {[columName: string]: ForeignKey} 19 | 20 | type PrimaryKeyInfo = { 21 | [columnName: string]: {datatype: string, isAutoIncrementing: boolean}; 22 | }; 23 | interface TableInfo { 24 | tableName: string; 25 | numberOfRows: number; 26 | numberOfIndexes: number; 27 | numberOfFields: number; 28 | numberOfForeignKeys: number; 29 | foreignKeysObj: ForeignKeyInfo|null; 30 | primaryKeysObj: PrimaryKeyInfo | null; 31 | } 32 | 33 | interface CheckConstraint { 34 | constraint_name: string; 35 | column_name: string; 36 | constraint_definition: string; 37 | } 38 | 39 | interface CheckConstraintMap { 40 | [columnName: string]: string; 41 | } 42 | 43 | 44 | const dbInfoController: DbInfoController = { 45 | getDataBaseInfo: async (_req, res, next): Promise => { 46 | // console.log('made it in dbinfo'); 47 | // pulling database connection from res locals 48 | const db = res.locals.dbConnection; 49 | 50 | try { 51 | // Retrievie table names 52 | const tables: QueryResult = await db.query( 53 | 'SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';' 54 | ); 55 | 56 | // Check if we have at least one table to work with 57 | if (tables.rows.length === 0) { 58 | res.locals.result = 'No tables found in database.'; 59 | return next(); 60 | } 61 | 62 | //array of table names to use in generic metrics function 63 | const tableNames = tables.rows.map(obj => obj.tablename); 64 | 65 | // passing tableNames through res locals 66 | res.locals.tableNames = tableNames; 67 | 68 | //creating an array of comprehensive info on the tables 69 | const tableInfoPromises: Promise[] = tables.rows.map(async (row: any) => { 70 | const tableName = row.tablename; 71 | 72 | const numberOfFields: QueryResult = await db.query( 73 | 'SELECT COUNT(*) FROM information_schema.columns WHERE table_name = $1;', 74 | [tableName] 75 | ); 76 | 77 | const numberOfRows: QueryResult = await db.query( 78 | `SELECT COUNT(*) FROM ${tableName};` 79 | ); 80 | 81 | const numberOfIndexes: QueryResult = await db.query( 82 | 'SELECT COUNT(*) FROM pg_indexes WHERE tablename = $1;', 83 | [tableName] 84 | ); 85 | 86 | const foreignKeys: QueryResult = await db.query(` 87 | SELECT 88 | kcu.column_name AS column, 89 | ccu.table_name AS referencedTable, 90 | ccu.column_name AS referencedColumn 91 | FROM 92 | information_schema.table_constraints AS tc 93 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name 94 | JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name 95 | WHERE 96 | tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1`, 97 | [tableName] 98 | ); 99 | const foreignKeyObject: ForeignKeyInfo = {}; 100 | 101 | for (const row of foreignKeys.rows) { 102 | foreignKeyObject[row.column] = { 103 | column: row.column, 104 | referencedTable: row.referencedtable, 105 | referencedColumn: row.referencedcolumn 106 | }; 107 | } 108 | 109 | const primaryKeys: QueryResult = await db.query(` 110 | SELECT 111 | kcu.column_name AS column, 112 | cols.data_type AS data_type, 113 | CASE 114 | WHEN cols.column_default LIKE 'nextval%' THEN TRUE 115 | ELSE FALSE 116 | END AS is_auto_incrementing 117 | FROM 118 | information_schema.table_constraints AS tc 119 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name 120 | JOIN information_schema.columns AS cols ON tc.table_name = cols.table_name AND kcu.column_name = cols.column_name 121 | WHERE 122 | tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = $1;`, 123 | [tableName] 124 | ); 125 | 126 | const primaryKeyObject: PrimaryKeyInfo = {}; 127 | // creating an object of the column name as the key and { datatype, isAutoIncrementing } as the value 128 | for (const row of primaryKeys.rows) { 129 | primaryKeyObject[row.column] = { 130 | datatype: row.data_type, 131 | isAutoIncrementing: row.is_auto_incrementing 132 | }; 133 | } 134 | 135 | const sampleData: QueryResult = await db.query( 136 | `SELECT * FROM ${tableName} LIMIT 100;` 137 | ); 138 | 139 | const columnDataTypes: QueryResult = await db.query(` 140 | SELECT column_name, data_type, column_default, is_nullable 141 | FROM information_schema.columns 142 | WHERE table_name = $1 AND table_schema = 'public' 143 | ORDER BY ordinal_position;`, 144 | [tableName] 145 | ); 146 | 147 | // Now, transform 'result.rows' to the desired format 148 | // const fieldTypes: { [key: string]: string } = {}; 149 | 150 | // for (const row of columnDataTypes.rows) { 151 | // fieldTypes[row.column_name] = row.data_type; 152 | // } 153 | 154 | // A check constrain is a constraint on a column that requires only specific values be inserted (i.e. "Yes" or "No") 155 | const checkContraints = await db.query(` 156 | SELECT 157 | conname AS constraint_name, 158 | a.attname AS column_name, 159 | con.conkey AS constraint_definition 160 | FROM 161 | pg_constraint con 162 | INNER JOIN 163 | pg_class rel ON rel.oid = con.conrelid 164 | INNER JOIN 165 | pg_attribute a ON a.attnum = ANY(con.conkey) 166 | WHERE 167 | con.contype = 'c' AND rel.relname = $1;`, 168 | [tableName] 169 | ); 170 | const checkContraintObj: CheckConstraintMap = {}; 171 | checkContraints.rows.forEach((checkEl : CheckConstraint) => { 172 | checkContraintObj[checkEl.column_name] = checkEl.constraint_definition; 173 | }); 174 | 175 | return { 176 | tableName, 177 | numberOfRows: parseInt(numberOfRows.rows[0].count, 10), 178 | numberOfIndexes: parseInt(numberOfIndexes.rows[0].count, 10), 179 | numberOfFields: parseInt(numberOfFields.rows[0].count, 10), 180 | numberOfForeignKeys: foreignKeys.rowCount, 181 | numberOfPrimaryKeys: primaryKeys.rowCount, 182 | checkConstraints: checkContraintObj, 183 | foreignKeysObj: foreignKeyObject || {}, 184 | primaryKeysObj: primaryKeyObject || {}, 185 | sampleData: sampleData.rows[sampleData.rows.length-1] || {}, // Sample data for the table 186 | columnDataTypes: columnDataTypes.rows.map((obj) => { 187 | return ({column_name: obj.column_name, datatype: obj.data_type}); 188 | }) // Column names and their data types 189 | }; 190 | }); 191 | // Usage 192 | // use Promise.all() to wait for all promises to resolve 193 | // const databaseInfo = await Promise.all(tableInfoPromises); 194 | Promise.all(tableInfoPromises).then((databaseInfo) => { 195 | const databaseInfoMap: { [key: string]: TableInfo } = {}; 196 | databaseInfo.forEach(info => { 197 | databaseInfoMap[info.tableName] = info; 198 | }); 199 | res.locals.databaseInfo = databaseInfoMap; 200 | return next(); 201 | }); 202 | 203 | } catch (error) { 204 | return next(error); 205 | } 206 | } 207 | }; 208 | 209 | export default dbInfoController; -------------------------------------------------------------------------------- /server/controllers/dbOverviewConroller.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type DbOverviewController = { 3 | dbSizeMetrics: RequestHandler; 4 | }; 5 | declare const dbOverviewController: DbOverviewController; 6 | export default dbOverviewController; 7 | -------------------------------------------------------------------------------- /server/controllers/dbOverviewConroller.js: -------------------------------------------------------------------------------- 1 | const dbOverviewController = { 2 | dbSizeMetrics: async (_req, res, next) => { 3 | const db = res.locals.dbConnection; 4 | try { 5 | const tables = await db.query(` 6 | SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema' 7 | `); 8 | // const tables: QueryResult = await db.query( 9 | // 'SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\';' 10 | // ); 11 | //array of table names to use in generic metrics function 12 | const tableNames = tables.rows.map(obj => obj.tablename); 13 | // const tableNames: string[] = res.locals.tableNames; // Assuming you have the table names in dbInfo 14 | // Check if we have at least one table to work with 15 | // Total database size 16 | const totalSizeQuery = await db.query('SELECT pg_size_pretty(pg_database_size(current_database())) as size;'); 17 | const totalDatabaseSize = totalSizeQuery.rows[0].size; 18 | // //Find the Schema name 19 | // const schemaNameRes: QueryResult = await db.query(`SELECT nspname 20 | // FROM pg_namespace 21 | // WHERE nspname NOT LIKE 'pg_%' 22 | // AND nspname != 'information_schema';`); 23 | const tableSizes = {}; 24 | for (const table of tableNames) { 25 | const sizeQuery = await db.query(`SELECT pg_size_pretty(pg_total_relation_size('${table}')) as diskSize, 26 | pg_size_pretty(pg_relation_size('${table}')) as rowSize`); 27 | if (sizeQuery.rows && sizeQuery.rows[0]) { 28 | tableSizes[table] = { 29 | diskSize: sizeQuery.rows[0].disksize, 30 | rowSize: sizeQuery.rows[0].rowsize, 31 | }; 32 | } 33 | } 34 | // Size of each index 35 | const indexSizeResults = await db.query(` 36 | SELECT 37 | indexrelname AS indexname, 38 | pg_size_pretty(pg_total_relation_size(indexrelid::regclass)) AS index_size 39 | FROM 40 | pg_stat_all_indexes 41 | JOIN 42 | pg_class ON pg_class.oid = pg_stat_all_indexes.indexrelid 43 | WHERE 44 | schemaname = 'public'; -- adjust for your schema if different 45 | `); //we will need to generalize publ 46 | //size of each index for each table 47 | // Size of each index 48 | const indexSizesRes = await Promise.all(tableNames.map(async (table) => { 49 | const sizeQuery = await db.query(`SELECT indexname, pg_size_pretty(pg_relation_size(indexname::regclass)) as size 50 | FROM pg_indexes 51 | WHERE tablename = $1;`, [table]); 52 | const indexes = sizeQuery.rows.reduce((acc, curr) => { 53 | acc[curr.indexname] = curr.size; 54 | return acc; 55 | }, {}); 56 | return { 57 | [table]: indexes, 58 | }; 59 | })); 60 | const indexSizesByTable = Object.assign({}, ...indexSizesRes); 61 | // Convert results into a key-value pair object 62 | const allIndexSizes = {}; 63 | for (const row of indexSizeResults.rows) { 64 | allIndexSizes[row.indexname] = row.index_size; 65 | } 66 | ///////////////////////////////////////////////////////////// 67 | // Log file size 68 | // const logSizeQuery: QueryResult = await db.query( 69 | // ); 'SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), \'0/0\'::pg_lsn) * 8192) AS wal_size;'; 70 | const logSize = '0'; //logSizeQuery.rows[0].wal_size; 71 | //current active connections 72 | const dbNameRes = await db.query('SELECT current_database()'); 73 | const dbName = dbNameRes.rows[0].current_database; 74 | const activeConnectionsRes = await db.query(` 75 | SELECT COUNT(*) 76 | FROM pg_stat_activity 77 | WHERE datname = $1; 78 | `, [dbName]); 79 | const activeConnections = activeConnectionsRes.rows[0].count; 80 | // Building the result object 81 | const dbSizeMetrics = { 82 | tableNames, 83 | totalDatabaseSize, 84 | tableSizes, 85 | allIndexSizes, 86 | indexSizesByTable, 87 | freeSpace: 'To be determined (Requires OS-level command or access to pg_settings)', 88 | logSize, 89 | activeConnections 90 | }; 91 | res.locals.dbSizeMetrics = dbSizeMetrics; 92 | return next(); 93 | } 94 | catch (error) { 95 | return next(error); 96 | } 97 | }, 98 | /* 99 | dbPerformanceMetrics: async (req, res, next): Promise => { 100 | const db = res.locals.dbConnection; 101 | 102 | try { 103 | // 1. Average query response time 104 | const avgQueryResponseTimeRes: QueryResult = await db.query( 105 | `SELECT avg(total_exec_time) AS avg_exec_time 106 | FROM pg_stat_statements;` 107 | ); 108 | const avgQueryResponseTime = avgQueryResponseTimeRes.rows[0].avg_exec_time; 109 | 110 | // 2. Number of queries executed per second (you might need to track this over time) 111 | // const versionResult: QueryResult = await db.query('SHOW server_version_num;'); 112 | // const versionNumber = parseInt(versionResult.rows[0].server_version_num, 10); 113 | 114 | // let queryStr = ''; 115 | 116 | // if (versionNumber < 100000) { 117 | // // For PostgreSQL versions earlier than 10 118 | // queryStr = 'SELECT ... , stats_reset, ... FROM pg_stat_database ...'; 119 | // } else { 120 | // // For PostgreSQL versions 10 and later 121 | // queryStr = 'SELECT ... , stats_io_reset, ... FROM pg_stat_database ...'; 122 | // } 123 | 124 | const result: QueryResult = await db.query(queryStr); 125 | const queriesPerSecond = result.rows[0].qps; 126 | ///tracking over time 127 | 128 | const overTime: QueryResult = await db.query(` 129 | SELECT 130 | capture_time, 131 | SUM(calls) as total_queries, 132 | AVG(total_time/calls) as average_query_time, 133 | SUM(calls) / EXTRACT(EPOCH FROM (MAX(capture_time) - MIN(capture_time))) as queries_per_second 134 | FROM query_metrics 135 | GROUP BY capture_time 136 | ORDER BY capture_time DESC 137 | `); 138 | // 3. Slowest queries and their execution times 139 | const slowestQueriesRes: QueryResult = await db.query( 140 | `SELECT query, total_exec_time / calls AS avg_exec_time 141 | FROM pg_stat_statements 142 | ORDER BY avg_exec_time DESC 143 | LIMIT 5;` 144 | ); 145 | const slowestQueries = slowestQueriesRes.rows.map(row => ({ 146 | query: row.query, 147 | executionTime: row.avg_exec_time, 148 | })); 149 | 150 | // 4. Cache hit rate 151 | const cacheHitRateRes: QueryResult = await db.query( 152 | `SELECT 153 | sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as rate 154 | FROM pg_statio_user_tables;` 155 | ); 156 | const cacheHitRate = cacheHitRateRes.rows[0].rate; 157 | 158 | const dbPerformanceMetrics: DbPerformanceMetrics = { 159 | avgQueryResponseTime, 160 | queriesPerSecond, 161 | slowestQueries, 162 | cacheHitRate, 163 | }; 164 | 165 | res.locals.dbPerformanceMetrics = dbPerformanceMetrics; 166 | return next(); 167 | 168 | } catch (error) { 169 | return next(error); 170 | } 171 | }, 172 | */ 173 | }; 174 | export default dbOverviewController; 175 | -------------------------------------------------------------------------------- /server/controllers/genericMetricsController.d.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | type GeneralMetricsController = { 3 | performGenericQueries: RequestHandler; 4 | }; 5 | declare const dbGenericQueryTesting: GeneralMetricsController; 6 | export default dbGenericQueryTesting; 7 | -------------------------------------------------------------------------------- /server/controllers/genericMetricsController.js: -------------------------------------------------------------------------------- 1 | ///Using for helper functions on delete 2 | // interface ForeignKey { 3 | // column: string; 4 | // referencedTable: string; 5 | // referencedColumn: string; 6 | // } 7 | // type ForeignKeyInfo = { [columName: string]: ForeignKey } 8 | // type PrimaryKeyInfo = { 9 | // [columnName: string]: { datatype: string, isAutoIncrementing: boolean }; 10 | // }; 11 | // interface TableInfo { 12 | // tableName: string; 13 | // numberOfRows: number; 14 | // numberOfIndexes: number; 15 | // numberOfFields: number; 16 | // numberOfForeignKeys: number; 17 | // foreignKeysObj: ForeignKeyInfo 18 | // primaryKeysObj: PrimaryKeyInfo 19 | // } 20 | // interface DBinfo { 21 | // [tableName: string]: TableInfo; 22 | // } 23 | const dbGenericQueryTesting = { 24 | performGenericQueries: async (_req, res, next) => { 25 | const db = res.locals.dbConnection; 26 | const tableNames = res.locals.tableNames; 27 | const dbInfo = res.locals.databaseInfo; 28 | const executionPlans = {}; 29 | // await db.query('BEGIN'); // Start the transaction 30 | try { 31 | for (const tableName of tableNames) { 32 | //Initialize exec plans for the table 33 | executionPlans[tableName] = {}; 34 | // executionPlans[tableName].DELETE ={query: 'not applicable yet'}; 35 | // executionPlans[tableName].INSERT ={query: 'not applicable yet'}; 36 | const tableInfo = dbInfo[tableName]; 37 | const primaryKeysObject = tableInfo.primaryKeysObj; 38 | // const checkContraintObj = tableInfo.checkConstraints; 39 | const foreignKeysObj = tableInfo.foreignKeysObj; 40 | const sampleData = tableInfo.sampleData; 41 | // const columnDataTypes = tableInfo.columnDataTypes; 42 | const pkArray = [...Object.keys(primaryKeysObject)]; 43 | const sampleValuesArr = Object.values(sampleData); 44 | const sampleColumnsArr = Object.keys(sampleData); 45 | const unchangedSample = { ...sampleData }; 46 | //SELECT Test 47 | // SELECT test we make a select from the sample we pulled in the dbinfo tab 48 | const selectQuery = `SELECT * FROM ${tableInfo.tableName} WHERE '${sampleColumnsArr[sampleColumnsArr.length - 1]}' = $1`; 49 | const selectPlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${selectQuery}`, [sampleValuesArr[sampleValuesArr.length - 1]]); 50 | executionPlans[tableName].SELECT = { query: selectQuery, plan: selectPlan }; 51 | ///////////////// 52 | //UPDATe Test 53 | //we want to update the sample row by changing only the non constrained column values 54 | let updateColumn = sampleColumnsArr[0]; 55 | let updateValue = sampleValuesArr[0]; 56 | let col; 57 | const columns = Object.keys(sampleData); 58 | for (let i = 0; i < columns.length; i++) { 59 | col = columns[i]; 60 | if (!primaryKeysObject[col] && !foreignKeysObj[col]) { 61 | updateColumn = col; 62 | updateValue = unchangedSample[col]; 63 | break; 64 | } 65 | } 66 | const updateQuery = `UPDATE ${tableInfo.tableName} SET ${updateColumn} = $1 WHERE ${pkArray[pkArray.length - 1]} = $2`; 67 | if (updateColumn && updateValue) { 68 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, unchangedSample[pkArray[pkArray.length - 1]]]); 69 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 70 | } 71 | else { 72 | executionPlans[tableName].UPDATE = { query: `Table ${tableName} has no rows` }; 73 | } 74 | //done with update 75 | } 76 | res.locals.executionPlans = executionPlans; 77 | return next(); 78 | } 79 | catch (error) { 80 | //Rollback if an error is caught 81 | // await db.query('ROLLBACK'); 82 | return next({ 83 | log: `ERROR caught in generalMetricsController.performGenericQueries: ${error}`, 84 | status: 400, 85 | message: 'ERROR: error has occurred in generalMetricsController.performGenericQueries', 86 | }); 87 | } 88 | } 89 | }; 90 | export default dbGenericQueryTesting; 91 | /*//UPDATe Test 92 | //we want to update the sample row by changing only the non constrained column values 93 | const columns = Object.keys(sampleData); 94 | let updateColumn; 95 | let updateValue; 96 | let col; 97 | for(let i = 0; i < columns.length; i++){ 98 | col = columns[i]; 99 | if(!primaryKeysObject[col] && !foreignKeysObj[col] && !checkContraintObj[col]){ 100 | updateColumn = col; 101 | updateValue = sampleData[col]; 102 | } 103 | } 104 | for(const column of Object.keys(unchangedSample)){ 105 | if(!primaryKeysObject[column] && !foreignKeysObj[column]){ 106 | updateColumn = column; 107 | updateValue = unchangedSample[column]; 108 | break; 109 | } 110 | } 111 | const updateQuery = `UPDATE ${tableInfo.tableName} SET ${updateColumn} = $1 WHERE ${pkArray[pkArray.length - 1]} = $2`; 112 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, unchangedSample[pkArray[pkArray.length - 1]]]); 113 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 114 | //UPDATE TESTING 115 | 116 | // if(up) 117 | // // const updateQuery = `UPDATE ${tableName} SET "${updateColumn}" = '${String(updateValue)}' WHERE "${updateColumn}" = '${String(updateValue)}'`; 118 | 119 | // const updateQuery = `UPDATE ${tableName} SET '${updateColumn}' = ${updateValue} WHERE '${updateColumn}' = ${updateValue}`; 120 | // const updatePlan = await db.query( 121 | // `EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, 122 | // [String(updateValue), String(updateValue)] 123 | // ); 124 | // executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 125 | // //done with update 126 | } 127 | 128 | //SELECT TESTING 129 | // const selectColumn = */ 130 | -------------------------------------------------------------------------------- /server/controllers/genericMetricsController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | // import { explainQuery } from '../helpers/explainQuery'; 3 | import { QueryResult } from 'pg'; 4 | // import { faker } from '@faker-js/faker'; 5 | 6 | type GeneralMetricsController = { 7 | performGenericQueries: RequestHandler; 8 | }; 9 | type QueryResults = { 10 | query?: string; 11 | plan?: QueryResult; 12 | values?: any; 13 | otherExecutions?: TableResults 14 | }; 15 | 16 | type TableResults = { 17 | [operationName: string]: QueryResults 18 | }; 19 | 20 | type ExecutionPlans = { 21 | [tableName: string]: TableResults; 22 | }; 23 | ///Using for helper functions on delete 24 | // interface ForeignKey { 25 | // column: string; 26 | // referencedTable: string; 27 | // referencedColumn: string; 28 | // } 29 | // type ForeignKeyInfo = { [columName: string]: ForeignKey } 30 | // type PrimaryKeyInfo = { 31 | // [columnName: string]: { datatype: string, isAutoIncrementing: boolean }; 32 | // }; 33 | // interface TableInfo { 34 | // tableName: string; 35 | // numberOfRows: number; 36 | // numberOfIndexes: number; 37 | // numberOfFields: number; 38 | // numberOfForeignKeys: number; 39 | // foreignKeysObj: ForeignKeyInfo 40 | // primaryKeysObj: PrimaryKeyInfo 41 | // } 42 | // interface DBinfo { 43 | // [tableName: string]: TableInfo; 44 | // } 45 | 46 | const dbGenericQueryTesting: GeneralMetricsController = { 47 | performGenericQueries: async (_req, res, next) => { 48 | const db = res.locals.dbConnection; 49 | 50 | const tableNames = res.locals.tableNames; 51 | 52 | const dbInfo = res.locals.databaseInfo; 53 | 54 | const executionPlans: ExecutionPlans = {}; 55 | 56 | // await db.query('BEGIN'); // Start the transaction 57 | try { 58 | for (const tableName of tableNames) { 59 | //Initialize exec plans for the table 60 | executionPlans[tableName] = {}; 61 | // executionPlans[tableName].DELETE ={query: 'not applicable yet'}; 62 | // executionPlans[tableName].INSERT ={query: 'not applicable yet'}; 63 | const tableInfo = dbInfo[tableName]; 64 | const primaryKeysObject = tableInfo.primaryKeysObj; 65 | // const checkContraintObj = tableInfo.checkConstraints; 66 | const foreignKeysObj = tableInfo.foreignKeysObj; 67 | const sampleData = tableInfo.sampleData; 68 | // const columnDataTypes = tableInfo.columnDataTypes; 69 | const pkArray = [...Object.keys(primaryKeysObject)]; 70 | const sampleValuesArr = Object.values(sampleData); 71 | const sampleColumnsArr = Object.keys(sampleData); 72 | const unchangedSample = { ...sampleData }; 73 | //SELECT Test 74 | // SELECT test we make a select from the sample we pulled in the dbinfo tab 75 | const selectQuery = `SELECT * FROM ${tableInfo.tableName} WHERE '${sampleColumnsArr[sampleColumnsArr.length - 1]}' = $1`; 76 | const selectPlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${selectQuery}`, [sampleValuesArr[sampleValuesArr.length - 1]]); 77 | executionPlans[tableName].SELECT = { query: selectQuery, plan: selectPlan }; 78 | ///////////////// 79 | //UPDATe Test 80 | //we want to update the sample row by changing only the non constrained column values 81 | let updateColumn = sampleColumnsArr[0]; 82 | let updateValue = sampleValuesArr[0]; 83 | let col; 84 | const columns = Object.keys(sampleData); 85 | for (let i = 0; i < columns.length; i++) { 86 | col = columns[i]; 87 | if (!primaryKeysObject[col] && !foreignKeysObj[col]) { 88 | updateColumn = col; 89 | updateValue = unchangedSample[col]; 90 | break; 91 | } 92 | } 93 | 94 | const updateQuery = `UPDATE ${tableInfo.tableName} SET ${updateColumn} = $1 WHERE ${pkArray[pkArray.length - 1]} = $2`; 95 | if (updateColumn && updateValue) { 96 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, unchangedSample[pkArray[pkArray.length - 1]]]); 97 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 98 | } else { 99 | executionPlans[tableName].UPDATE = { query: `Table ${tableName} has no rows` }; 100 | } 101 | 102 | //done with update 103 | } 104 | res.locals.executionPlans = executionPlans; 105 | return next(); 106 | } 107 | catch (error) { 108 | //Rollback if an error is caught 109 | // await db.query('ROLLBACK'); 110 | return next({ 111 | log: `ERROR caught in generalMetricsController.performGenericQueries: ${error}`, 112 | status: 400, 113 | message: 'ERROR: error has occurred in generalMetricsController.performGenericQueries', 114 | }); 115 | } 116 | } 117 | }; 118 | 119 | export default dbGenericQueryTesting; 120 | 121 | /*//UPDATe Test 122 | //we want to update the sample row by changing only the non constrained column values 123 | const columns = Object.keys(sampleData); 124 | let updateColumn; 125 | let updateValue; 126 | let col; 127 | for(let i = 0; i < columns.length; i++){ 128 | col = columns[i]; 129 | if(!primaryKeysObject[col] && !foreignKeysObj[col] && !checkContraintObj[col]){ 130 | updateColumn = col; 131 | updateValue = sampleData[col]; 132 | } 133 | } 134 | for(const column of Object.keys(unchangedSample)){ 135 | if(!primaryKeysObject[column] && !foreignKeysObj[column]){ 136 | updateColumn = column; 137 | updateValue = unchangedSample[column]; 138 | break; 139 | } 140 | } 141 | const updateQuery = `UPDATE ${tableInfo.tableName} SET ${updateColumn} = $1 WHERE ${pkArray[pkArray.length - 1]} = $2`; 142 | const updatePlan = await db.query(`EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, [updateValue, unchangedSample[pkArray[pkArray.length - 1]]]); 143 | executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 144 | //UPDATE TESTING 145 | 146 | // if(up) 147 | // // const updateQuery = `UPDATE ${tableName} SET "${updateColumn}" = '${String(updateValue)}' WHERE "${updateColumn}" = '${String(updateValue)}'`; 148 | 149 | // const updateQuery = `UPDATE ${tableName} SET '${updateColumn}' = ${updateValue} WHERE '${updateColumn}' = ${updateValue}`; 150 | // const updatePlan = await db.query( 151 | // `EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true,FORMAT JSON) ${updateQuery}`, 152 | // [String(updateValue), String(updateValue)] 153 | // ); 154 | // executionPlans[tableName].UPDATE = { query: updateQuery, plan: updatePlan }; 155 | // //done with update 156 | } 157 | 158 | //SELECT TESTING 159 | // const selectColumn = */ -------------------------------------------------------------------------------- /server/routes/pgRoute.d.ts: -------------------------------------------------------------------------------- 1 | declare const pgRoute: import("express-serve-static-core").Router; 2 | export default pgRoute; 3 | -------------------------------------------------------------------------------- /server/routes/pgRoute.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dbConnectionController from '../controllers/dbConnectionController.js'; 3 | import dbInfoController from '../controllers/dbInfoController.js'; 4 | import dbERDcontroller from '../controllers/dbERDcontroller.js'; 5 | import genericMetricsController from '../controllers/genericMetricsController.js'; 6 | import dbOverviewController from '../controllers/dbOverviewConroller.js'; 7 | import dBHistoryController from '../controllers/dbHistoryController.js'; 8 | import customDBController from '../controllers/customQueryController.js'; 9 | const pgRoute = express.Router(); 10 | pgRoute.post('/dbInfo', dbConnectionController.connectAndInitializeDB, dbConnectionController.createExtension, dbInfoController.getDataBaseInfo, dbERDcontroller.getSchemaPostgreSQL, genericMetricsController.performGenericQueries, dbOverviewController.dbSizeMetrics, dBHistoryController.dbPastMetrics, (_req, res) => { 11 | return res.status(200).json(res.locals); 12 | }); 13 | pgRoute.post('/customQuery', customDBController.customQueryMetrics, 14 | //new controller 15 | (_req, res) => { 16 | return res.status(200).json(res.locals.customMetrics); 17 | }); 18 | export default pgRoute; 19 | -------------------------------------------------------------------------------- /server/routes/pgRoute.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dbConnectionController from '../controllers/dbConnectionController.js'; 3 | import dbInfoController from '../controllers/dbInfoController.js'; 4 | import dbERDcontroller from '../controllers/dbERDcontroller.js' 5 | import genericMetricsController from '../controllers/genericMetricsController.js'; 6 | import dbOverviewController from '../controllers/dbOverviewConroller.js'; 7 | import dBHistoryController from '../controllers/dbHistoryController.js'; 8 | import customDBController from '../controllers/customQueryController.js'; 9 | const pgRoute = express.Router(); 10 | 11 | pgRoute.post( 12 | '/dbInfo', 13 | dbConnectionController.connectAndInitializeDB, 14 | dbConnectionController.createExtension, 15 | dbInfoController.getDataBaseInfo, 16 | dbERDcontroller.getSchemaPostgreSQL, 17 | genericMetricsController.performGenericQueries, 18 | dbOverviewController.dbSizeMetrics, 19 | dBHistoryController.dbPastMetrics, 20 | (_req, res) => { 21 | return res.status(200).json(res.locals); 22 | } 23 | ); 24 | pgRoute.post( 25 | '/customQuery', 26 | customDBController.customQueryMetrics, 27 | //new controller 28 | 29 | (_req, res) => { 30 | return res.status(200).json(res.locals.customMetrics); 31 | } 32 | ); 33 | 34 | export default pgRoute; -------------------------------------------------------------------------------- /server/server.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | // allows the use __dirname in es module scope 4 | import { fileURLToPath } from 'url'; 5 | import pgRoute from './routes/pgRoute.js'; 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | const app = express(); 9 | const PORT = 3000; 10 | app.use(express.static(path.join(__dirname, '../dist/'))); 11 | app.use(express.json()); 12 | app.use(express.urlencoded({ extended: false })); 13 | // routes 14 | app.use('/api/pg', pgRoute); 15 | // unknown route handler 16 | app.use((_req, res) => { 17 | return res.status(404).send('No page found'); 18 | }); 19 | // global error handler 20 | app.use((err, _req, res, _next) => { 21 | const defaultErr = { 22 | log: 'Express error handler caught unknown middleware error', 23 | status: 500, 24 | message: { 25 | error: 'An error occured' 26 | } 27 | }; 28 | const errorObj = { ...defaultErr, ...err }; 29 | return res.status(errorObj.status).json(errorObj.message); 30 | }); 31 | app.listen(PORT, () => { 32 | console.log(`Server listening on port ${PORT}: http://localhost:${PORT}`); 33 | }); 34 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | 2 | import express, { Request, Response, NextFunction } from 'express'; 3 | import path from 'path'; 4 | // allows the use __dirname in es module scope 5 | import { fileURLToPath } from 'url'; 6 | import pgRoute from './routes/pgRoute.js'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const app = express(); 11 | const PORT = 3000; 12 | 13 | app.use(express.static(path.join(__dirname, '../dist/'))); 14 | app.use(express.json()); 15 | app.use(express.urlencoded({extended: false})); 16 | 17 | // routes 18 | app.use('/api/pg', pgRoute); 19 | 20 | // unknown route handler 21 | app.use((_req: Request, res: Response) => { 22 | return res.status(404).send('No page found'); 23 | }); 24 | 25 | // global error handler 26 | app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { 27 | const defaultErr = { 28 | log: 'Express error handler caught unknown middleware error', 29 | status: 500, 30 | message: { 31 | error: 'An error occured' 32 | } 33 | }; 34 | const errorObj = { ...defaultErr, ...err }; 35 | return res.status(errorObj.status).json(errorObj.message); 36 | }); 37 | 38 | 39 | app.listen(PORT, () => { 40 | console.log(`Server listening on port ${PORT}: http://localhost:${PORT}`); 41 | }); 42 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, createBrowserRouter, createRoutesFromElements, RouterProvider } from "react-router-dom"; 3 | import Home from "./pages/Home"; 4 | import Dashboard from "./pages/Dashboard"; 5 | import "./index.css"; 6 | import "./App.css"; 7 | import RouteError from "./components/Errors/RouteError"; 8 | import About from "./pages/About"; 9 | 10 | 11 | const router = createBrowserRouter( 12 | 13 | createRoutesFromElements( 14 | }> 15 | } /> 16 | } errorElement={} /> 17 | } /> 18 | } /> 19 | 20 | ) 21 | ) 22 | 23 | const App: React.FC = () => { 24 | return ( 25 | 26 | ) 27 | } 28 | 29 | function PageNotFound() { 30 | return ( 31 |
32 |
33 |

404

34 |

Page Not Found

35 |
36 |
37 | ) 38 | } 39 | 40 | export default App; 41 | 42 | -------------------------------------------------------------------------------- /src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink, Outlet } from 'react-router-dom'; 2 | 3 | export default function Layout() { 4 | return ( 5 |
6 | 10 | {/*
*/} 11 | 12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | export type RFState = { 2 | edges: any[]; 3 | setEdges: (eds: any) => void; 4 | nodes: any[]; 5 | setNodes: (nds: any) => void; 6 | onNodesChange: (changes: any) => void; 7 | onEdgesChange: (changes: any) => void; 8 | onConnect: (connection: any) => void; 9 | }; 10 | 11 | export type Edge = { 12 | id: string; 13 | source: string; 14 | target: string; 15 | style: {strokeWidth: number; stroke: string}; 16 | markerEnd: { 17 | type: string; 18 | width: number; 19 | height: number; 20 | color: string; 21 | }; 22 | type: string; 23 | }; 24 | 25 | export interface IndexItem { 26 | [key: string]: any; 27 | } 28 | 29 | export type RowsInfo = { 30 | tableName: string; 31 | numberOfRows: number; 32 | }; 33 | 34 | export type RowsInfoArray = RowsInfo[]; 35 | 36 | export type indexInfo = { 37 | tableName: string; 38 | numberOfIndexes: number; 39 | }; 40 | 41 | export type indexTableArray = indexInfo[]; 42 | -------------------------------------------------------------------------------- /src/assets/GIFs/CustomQuery_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/GIFs/CustomQuery_gif.gif -------------------------------------------------------------------------------- /src/assets/GIFs/Dashboard_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/GIFs/Dashboard_gif.gif -------------------------------------------------------------------------------- /src/assets/GIFs/ERD_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/GIFs/ERD_gif.gif -------------------------------------------------------------------------------- /src/assets/LogoIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/LogoIcon.png -------------------------------------------------------------------------------- /src/assets/devPhotos/Kurt.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/devPhotos/Kurt.jpeg -------------------------------------------------------------------------------- /src/assets/fk_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/fk_icon.png -------------------------------------------------------------------------------- /src/assets/logo-horizontal-v2-darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/logo-horizontal-v2-darkmode.png -------------------------------------------------------------------------------- /src/assets/logo-horizontal-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/logo-horizontal-v2.png -------------------------------------------------------------------------------- /src/assets/logov1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/logov1.png -------------------------------------------------------------------------------- /src/assets/pk_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/pk_icon.png -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/screenshots/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/screenshots/screenshot1.jpg -------------------------------------------------------------------------------- /src/assets/screenshots/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/screenshots/screenshot2.jpg -------------------------------------------------------------------------------- /src/assets/screenshots/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/screenshots/screenshot3.jpg -------------------------------------------------------------------------------- /src/assets/team_headshots/dkim_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/dkim_headshot.jpg -------------------------------------------------------------------------------- /src/assets/team_headshots/dmurcia_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/dmurcia_headshot.jpg -------------------------------------------------------------------------------- /src/assets/team_headshots/kbulau_headshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/kbulau_headshot.png -------------------------------------------------------------------------------- /src/assets/team_headshots/other_dkim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/other_dkim.jpg -------------------------------------------------------------------------------- /src/assets/team_headshots/sheck_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/sheck_headshot.jpg -------------------------------------------------------------------------------- /src/assets/team_headshots/ytalab_headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/team_headshots/ytalab_headshot.jpg -------------------------------------------------------------------------------- /src/assets/techstack_icons/expressjslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/expressjslogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/jestlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/jestlogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/nodelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/nodelogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/postgresqllogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/postgresqllogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/reactlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/reactlogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/tailwindlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/tailwindlogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/typescriptlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/typescriptlogo.png -------------------------------------------------------------------------------- /src/assets/techstack_icons/vitelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/assets/techstack_icons/vitelogo.png -------------------------------------------------------------------------------- /src/components/Errors/AppFallback.tsx: -------------------------------------------------------------------------------- 1 | import { FallbackProps } from "react-error-boundary"; 2 | 3 | 4 | export const AppFallback: React.FC = ({ error, resetErrorBoundary }) => { 5 | console.error(error) 6 | 7 | return ( 8 |
9 |

Something went wrong! Please try again

10 |

Error: {error.message}

11 | 15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /src/components/Errors/RouteError.tsx: -------------------------------------------------------------------------------- 1 | import { useErrorBoundary } from "react-error-boundary"; 2 | import { useRouteError } from "react-router-dom"; 3 | 4 | 5 | 6 | const RouteError: React.FC = () => { 7 | const error: any = useRouteError(); 8 | const { resetBoundary } = useErrorBoundary(); 9 | console.error(error) 10 | return ( 11 |
12 |
13 |

Something went wrong! Please try again

14 |

Error: {error.message}

15 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default RouteError; -------------------------------------------------------------------------------- /src/components/ReactFlow/RFTable.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, Position } from "reactflow"; 2 | import fk_icon from "../../assets/fk_icon.png"; 3 | import pk_icon from "../../assets/pk_icon.png"; 4 | 5 | export default function RFTable (nodeData: any) { 6 | const rows: JSX.Element[] = []; 7 | // console.log("data", data) 8 | // Columns is an object of columns with their properties 9 | const columns: {[key:string]: any} = nodeData.data.table[1]; 10 | for (const columnNames in columns){ 11 | const handle: JSX.Element[] = []; 12 | if (columns[columnNames].foreign_key === 'true'){ 13 | handle.push () 14 | } else if (columns[columnNames].primary_key === 'true'){ 15 | handle.push( ) 16 | } 17 | rows.push( 18 | 19 | {columns[columnNames].primary_key === 'true' ? : null } 20 | {columns[columnNames].foreign_key === 'true' ? : null}{handle} 21 | {columnNames} 22 | {columns[columnNames].data_type} 23 | {columns[columnNames].Constraints} 24 | 25 | 26 | ); 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 |
33 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {rows} 50 | 51 |
PKFKColumnTypeConstraints
52 |
53 |
54 | 55 | ); 56 | } -------------------------------------------------------------------------------- /src/components/ReactFlow/createEdges.ts: -------------------------------------------------------------------------------- 1 | import { Edge } from "../../Types"; 2 | export default function createEdges(ERDdata: any) { 3 | 4 | 5 | // ERDdata is an object of tables 6 | const initialEdges:Edge[] = []; 7 | for (const tableName in ERDdata){ 8 | const ColumnData = ERDdata[tableName] 9 | for (const columns in ColumnData){ 10 | if (ColumnData[columns].primary_key === 'true'){ 11 | const target = tableName; 12 | ColumnData[columns].foreign_tables.forEach((element: string) => { 13 | const source = element; 14 | initialEdges.push({ 15 | id: `${source}-${target}`, 16 | source: source, 17 | target: target, 18 | style: { 19 | strokeWidth: 1, 20 | stroke: 'white', 21 | }, 22 | markerEnd: { 23 | type: 'arrowclosed', 24 | width: 30, 25 | height: 40, 26 | color: '#535D71', 27 | }, 28 | type: 'smartStep', 29 | }); 30 | }); 31 | } 32 | } 33 | } 34 | return initialEdges; 35 | }; -------------------------------------------------------------------------------- /src/components/ReactFlow/createNodes.ts: -------------------------------------------------------------------------------- 1 | export default function createNodes(ERDdata: any) { 2 | const initialNodes: any = [] 3 | 4 | let x: number = 0; 5 | let y: number = 0; 6 | for (const tableName in ERDdata){ 7 | const columnData = ERDdata[tableName]; 8 | initialNodes.push({ 9 | id: tableName, 10 | type: 'table', 11 | position: {x, y}, 12 | data: {table: [tableName, columnData]}, 13 | }); 14 | 15 | x += 800; 16 | 17 | if (x > 2000) { 18 | x= 0; 19 | y += 600; 20 | } 21 | } 22 | 23 | return initialNodes; 24 | }; -------------------------------------------------------------------------------- /src/components/ReactFlow/flow.tsx: -------------------------------------------------------------------------------- 1 | import ReactFlow, { Background, Controls } from 'reactflow'; 2 | import { useEffect } from 'react'; 3 | import 'reactflow/dist/base.css'; 4 | import createNodes from './createNodes'; 5 | import createEdges from './createEdges'; 6 | import RFTable from './RFTable'; 7 | import useAppStore from '../../store/appStore'; 8 | import useFlowStore from '../../store/flowStore'; 9 | import { SmartBezierEdge } from '@tisoap/react-flow-smart-edge'; 10 | import { SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; 11 | import { SmartStepEdge } from '@tisoap/react-flow-smart-edge'; 12 | 13 | import '../../../tailwind.config.js' 14 | 15 | 16 | const nodeTypes = { 17 | table: RFTable, 18 | }; 19 | 20 | const edgeTypes = { 21 | smart: SmartBezierEdge, 22 | smartStraight: SmartStraightEdge, 23 | smartStep: SmartStepEdge 24 | } 25 | 26 | export default function Flow(): JSX.Element { 27 | const { edges, setEdges, nodes, setNodes, onNodesChange, onEdgesChange } = 28 | useFlowStore((state) => state); 29 | 30 | const { metricsData, theme } = useAppStore(); 31 | const masterData = metricsData.erDiagram; 32 | const initialData = createNodes(masterData); 33 | const initialEdges = createEdges(masterData); 34 | const proOptions = { hideAttribution: true }; 35 | 36 | 37 | 38 | 39 | useEffect(() => { 40 | setNodes(initialData); 41 | setEdges(initialEdges); 42 | }, [masterData, setNodes, setEdges]); 43 | 44 | return ( 45 |
46 | 55 | 56 | 57 | 58 |
59 | ); 60 | } -------------------------------------------------------------------------------- /src/components/charts/ColumnIndexSizes.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | import { IndexItem } from '../../Types'; 4 | 5 | export const ColumnIndexSizes: React.FC = () => { 6 | const { metricsData, toNumInKB, theme } = useAppStore(); 7 | const indexesArray: {}[] = Object.values( metricsData.dbSizeMetrics.indexSizesByTable); 8 | console.log(indexesArray); 9 | const labelsArray: string[] = []; 10 | const indexData: number[] = []; 11 | // loop through indexes Array 12 | // loop through keys in current object element of indexesArray 13 | // create an array of 14 | indexesArray.forEach((table: IndexItem): void => { 15 | for (const name in table) { 16 | labelsArray.push(name.slice(0, -2)); 17 | indexData.push(toNumInKB(table[name])); 18 | } 19 | }); 20 | 21 | const options: any = { 22 | indexAxis: 'y', 23 | responsive: true, 24 | maintainAspectRatio: false, 25 | layout: { 26 | padding: { 27 | top: 0, 28 | right: 0, 29 | bottom: 0, 30 | left: 0 31 | } 32 | }, 33 | plugins: { 34 | title: { 35 | display: true, 36 | text: 'Index Size by Column', 37 | color: theme === "light" ? '#17012866' : '#ffffffac', 38 | font: { 39 | size: 14 40 | }, 41 | padding: { 42 | top: 10, 43 | bottom: 10 44 | } 45 | }, 46 | legend: { 47 | display: true, 48 | position: 'bottom' as const, 49 | labels: { 50 | font: { 51 | size: 12, 52 | }, 53 | boxWidth: 10, 54 | padding: 10 55 | } 56 | } 57 | }, 58 | scales: { 59 | y: { 60 | beginAtZero: true, 61 | ticks: { 62 | padding: 0, 63 | autoSkip: false 64 | } 65 | }, 66 | // x: { 67 | // ticks: { 68 | // maxRotation: 90, // Set the maximum rotation angle to 90 69 | // minRotation: 90, // Set the minimum rotation angle to 90 70 | // font: { 71 | // size: 10 72 | // }, 73 | // } 74 | // } 75 | } 76 | }; 77 | 78 | const data = { 79 | labels: labelsArray, 80 | datasets: [ 81 | { 82 | label: 'Index Size (kb)', 83 | data: indexData, 84 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 85 | scaleFontColor: '#FFFFFF', 86 | }, 87 | ] 88 | }; 89 | return ( 90 |
91 | 92 |
93 | ); 94 | }; -------------------------------------------------------------------------------- /src/components/charts/DbSizeCards.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useAppStore from "../../store/appStore"; 3 | // import { TableInfo } from '../../store/appStore' 4 | 5 | 6 | export const DBSizeCards: React.FC = () => { 7 | const { metricsData, toNumInKB } = useAppStore(); 8 | 9 | // total Database Size 10 | const databaseSizeTotal = toNumInKB(metricsData.dbSizeMetrics.totalDatabaseSize); 11 | console.log('databaseSizeTotal', databaseSizeTotal) 12 | 13 | // Active Connections 14 | const activeConnections = metricsData.dbSizeMetrics.activeConnections; 15 | console.log('activeConnections', activeConnections) 16 | 17 | console.log('metricsData', metricsData); 18 | 19 | 20 | return ( 21 | 22 | <> 23 | 24 |
25 |
26 |
Database Size (kbs)
27 |

{databaseSizeTotal}

28 |
29 |
30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/charts/GeneralMetrics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useAppStore from "../../store/appStore"; 3 | import { TableInfo } from '../../store/appStore' 4 | 5 | 6 | export const GeneralMetrics: React.FC = () => { 7 | const { metricsData, toNumInKB } = useAppStore(); 8 | const tablesArray: TableInfo[] = Object.values(metricsData.databaseInfo); 9 | const rows = tablesArray.map(table => { 10 | return { 11 | tableName: table.tableName, 12 | numberOfForeignKeys: table.numberOfForeignKeys, 13 | numberOfFields: table.numberOfFields, 14 | numberOfPrimaryKeys:table.numberOfPrimaryKeys 15 | } 16 | }) 17 | // total Foreign Keys 18 | const totalForeignKeys = rows.reduce((sum, table) => sum + table.numberOfForeignKeys, 0); 19 | const totalPrimaryKeys = rows.reduce((sum, table) => sum + table.numberOfPrimaryKeys, 0); 20 | // average Foreign Keys per table 21 | const averageForeignKeys = (totalForeignKeys / rows.length).toFixed(2); 22 | const averagePrimaryKeys = (totalPrimaryKeys / rows.length).toFixed(2); 23 | // console.log(`Total foreign keys: ${totalForeignKeys}`) 24 | // console.log(`Average number of foreign keys: ${averageForeignKeys}`) 25 | 26 | // total Number of Fields 27 | 28 | const totalNumberOfFields = rows.reduce((sum, table) => sum + table.numberOfFields, 0); 29 | 30 | // const averageNumberOfFields = totalNumberOfFields / rows.length; 31 | 32 | // console.log(`Total foreign keys: ${totalNumberOfFields}`) 33 | // console.log(`Average number of foreign keys: ${averageNumberOfFields}`) 34 | 35 | // total Database Size 36 | const databaseSizeTotal = toNumInKB(metricsData.dbSizeMetrics.totalDatabaseSize); 37 | console.log('databaseSizeTotal', databaseSizeTotal) 38 | 39 | // Active Connections 40 | const activeConnections = metricsData.dbSizeMetrics.activeConnections; 41 | console.log('activeConnections', activeConnections) 42 | 43 | console.log('metricsData', metricsData); 44 | 45 | 46 | // const tableTags: element[] = tablesArray.map(table => { 47 | // return( 48 | //

{`${table.tableName}: ${table.numberOfFields}`}

49 | // ) 50 | // }) 51 | 52 | 53 | 54 | return ( 55 | 56 | <> 57 |
58 |
59 |
60 |
Total Number Of Tables
61 |

{tablesArray.length}

62 |
63 |
64 |
65 |
66 |
Total Number Of Fields
67 |

{totalNumberOfFields}

68 |
69 |
70 |
71 |
72 |
73 |
74 |
Avg. Primary Keys Per Table
75 |

{averagePrimaryKeys}

76 |
77 |
78 |
79 |
80 |
Avg. Foreign Keys Per Table
81 |

{averageForeignKeys}

82 |
83 |
84 |
85 | 86 | 87 | ); 88 | } 89 | {/*
Avg. Foreign Keys Per Table
90 |

{averageForeignKeys}

*/} -------------------------------------------------------------------------------- /src/components/charts/IndexPerTable.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import { Chart as ChartJS, ArcElement, Tooltip, Legend, ChartOptions } from 'chart.js'; 3 | import React from 'react'; 4 | import useAppStore from '../../store/appStore'; 5 | import { TableInfo } from '../../store/appStore'; 6 | 7 | ChartJS.register(ArcElement, Tooltip, Legend); 8 | 9 | 10 | 11 | export const IndexPerTable: React.FC = () => { 12 | const { metricsData, theme } = useAppStore(); 13 | const tablesArray: TableInfo[] = Object.values(metricsData.databaseInfo); 14 | const indexData = tablesArray.map(table => { 15 | return { 16 | tableName: table.tableName, 17 | numberOfIndexes: table.numberOfIndexes, 18 | }; 19 | }); 20 | // Sort the indexData array based on the numberOfIndexes in descending order 21 | indexData.sort((a, b) => b.numberOfIndexes - a.numberOfIndexes); 22 | 23 | const options: ChartOptions = { 24 | indexAxis: "y", 25 | responsive: true, 26 | maintainAspectRatio: false, 27 | layout: { 28 | padding: 0 29 | }, 30 | plugins: { 31 | title: { 32 | display: true, 33 | text: 'Indexes Per Table', 34 | color: theme === "light" ? '#17012866' : '#ffffffac', 35 | font: { 36 | size: 14 37 | } 38 | }, 39 | legend: { 40 | display: true, 41 | position: 'bottom' as const, 42 | labels:{ 43 | font: { 44 | size: 12 45 | }, 46 | }, 47 | }, 48 | 49 | }, 50 | }; 51 | const data = { 52 | labels: indexData.map(table => table.tableName), 53 | datasets: [ 54 | { 55 | label: 'Number of Indexes', 56 | data: indexData.map(table => table.numberOfIndexes), 57 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 58 | scaleFontColor: '#FFFFFF', 59 | // backgroundColor: [ 60 | // 'rgba(190, 99, 255, 0.2)', 61 | // 'rgba(54, 162, 235, 0.2)', 62 | // 'rgba(235, 86, 255, 0.2)', 63 | // 'rgba(16, 39, 215, 0.2)', 64 | // 'rgba(129, 75, 236, 0.2)', 65 | // 'rgba(64, 118, 255, 0.2)', 66 | // ], 67 | borderColor: [ 68 | '#dbdbdbdf', 69 | // "rgba(54, 162, 235, 1)", 70 | // "rgba(255, 206, 86, 1)", 71 | // "rgba(75, 192, 192, 1)", 72 | // "rgba(153, 102, 255, 1)", 73 | // "rgba(255, 159, 64, 1)", 74 | ], 75 | borderWidth: 1, 76 | } 77 | ] 78 | }; 79 | return ( 80 |
81 | 82 |
83 | ); 84 | }; -------------------------------------------------------------------------------- /src/components/charts/PolarChart.tsx: -------------------------------------------------------------------------------- 1 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; 2 | import { PolarArea } from 'react-chartjs-2'; 3 | import { RowsInfoArray } from '../../Types'; 4 | import React from 'react'; 5 | 6 | ChartJS.register(ArcElement, Tooltip, Legend); 7 | 8 | interface PolarChartProps { 9 | rowsInfoData: RowsInfoArray; 10 | } 11 | 12 | 13 | // Rows per Table Polar Chart 14 | 15 | export const PolarChart: React.FC = ({ rowsInfoData }) => { 16 | const options = { //moved options into functions 17 | responsive: true, 18 | plugins: { 19 | legend: { 20 | position: 'top' as const, 21 | }, 22 | title: { 23 | display: true, 24 | text: 'Rows Per Table', 25 | color: '#ffffffc8', 26 | font: { 27 | size: 10 28 | } 29 | }, 30 | }, 31 | }; 32 | 33 | const data = { 34 | labels: rowsInfoData.map(table => table.tableName), 35 | datasets: [ 36 | { 37 | label: 'Rows Per Table', 38 | data: rowsInfoData.map(table => table.numberOfRows), 39 | backgroundColor: [ 40 | 'rgba(190, 99, 255, 0.2)', 41 | 'rgba(54, 162, 235, 0.2)', 42 | 'rgba(235, 86, 255, 0.2)', 43 | 'rgba(16, 39, 215, 0.2)', 44 | 'rgba(129, 75, 236, 0.2)', 45 | 'rgba(64, 118, 255, 0.2)', 46 | ], 47 | } 48 | ] 49 | }; 50 | return ( 51 |
52 | 53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/charts/QueryTimes.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | 4 | interface BarGraphProps { 5 | table: any; 6 | tableName: String; 7 | } 8 | 9 | export const QueryTimes: React.FC = ({ table, tableName }) => { 10 | const {theme} = useAppStore(); 11 | 12 | const executionTimes = [ 13 | table.SELECT.plan.rows[0]['QUERY PLAN'][0]['Execution Time'] * 1000, 14 | table.UPDATE.plan.rows[0]['QUERY PLAN'][0]['Execution Time'] * 1000 15 | ]; 16 | const planningTimes = [ 17 | table.SELECT.plan.rows[0]['QUERY PLAN'][0]['Planning Time'] * 1000, 18 | table.UPDATE.plan.rows[0]['QUERY PLAN'][0]['Planning Time'] * 1000 19 | ]; 20 | const totalTimes = [ 21 | executionTimes[0] + planningTimes[0], 22 | executionTimes[1] + planningTimes[1], 23 | ]; 24 | 25 | const options = { 26 | responsive: true, 27 | maintainAspectRatio: false, 28 | plugins: { 29 | title: { 30 | display: true, 31 | text: `Planning vs Execution Times - ${tableName}`, 32 | color: theme === "light" ? '#17012866' : '#ffffffac', 33 | font: { 34 | size: 14 35 | } 36 | }, 37 | legend: { 38 | display: true, 39 | position: 'bottom' as const, 40 | labels:{ 41 | font: { 42 | size: 12 43 | } 44 | } 45 | }, 46 | tooltip: { 47 | // enabled: false, // disable default tooltips 48 | // backgroundColor: 'rgb(255, 0, 200)', 49 | // titleColor:'rgb(0,0,255)', 50 | padding: { 51 | left: 3, 52 | right: 3, 53 | top: 2, 54 | bottom: 2 55 | }, 56 | bodyFont: { 57 | size: 8 58 | }, 59 | titleFont: { 60 | size: 10 61 | }, 62 | // displayColors: false, 63 | callbacks:{ 64 | afterLabel: function(context: any) { 65 | // Assuming that execution count is stored in an array 66 | const queryString = context.dataIndex === 0? "Select Query: `EXPLAIN SELECT * FROM ${tableInfo.tableName} WHERE '${sampleColumnsArr[sampleColumnsArr.length - 1]}' = $1`" : "Update Query: `EXPLAIN UPDATE ${tableInfo.tableName} SET ${updateColumn} = $1 WHERE ${pkArray[pkArray.length - 1]} = $2`" 67 | 68 | 69 | return [ 70 | queryString 71 | ]; 72 | }, 73 | }, 74 | }, 75 | }, 76 | scales: { 77 | y: { 78 | beginAtZero: true, 79 | } 80 | }, 81 | }; 82 | 83 | const data = { 84 | innerHeight: 100, 85 | labels: ['Select', 'Update'], 86 | datasets: [ 87 | { 88 | label: 'Planning Time (ms)', 89 | data: planningTimes, 90 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 91 | scaleFontColor: "#FFFFFF", 92 | }, 93 | { 94 | label: 'Execution Time (ms)', 95 | data: executionTimes, 96 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 97 | scaleFontColor: "#FFFFFF", 98 | }, 99 | { 100 | label: 'Total Time (ms)', 101 | data: totalTimes, 102 | backgroundColor: 'rgba(235, 86, 255, 0.2)', 103 | scaleFontColor: "#FFFFFF", 104 | }, 105 | ], 106 | }; 107 | return ( 108 |
109 | 110 |
111 | ); 112 | } -------------------------------------------------------------------------------- /src/components/charts/RowsPerTable.tsx: -------------------------------------------------------------------------------- 1 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; 2 | import { Pie } from 'react-chartjs-2'; 3 | import React from 'react'; 4 | import useAppStore from '../../store/appStore'; 5 | import { TableInfo } from '../../store/appStore'; 6 | 7 | 8 | ChartJS.register(ArcElement, Tooltip, Legend); 9 | 10 | 11 | export const RowsPerTable: React.FC = () => { 12 | const { metricsData, openModal, theme } = useAppStore(); 13 | const tablesArray: TableInfo[] = Object.values(metricsData.databaseInfo); 14 | const rows = tablesArray.map(table => { 15 | return { 16 | tableName: table.tableName, 17 | numberOfRows: table.numberOfRows, 18 | }; 19 | }); 20 | const options = { 21 | responsive: true, 22 | maintainAspectRatio: false, 23 | plugins: { 24 | legend: { 25 | display: true, 26 | position: 'left' as const, 27 | labels: { 28 | font: { 29 | size: 11 30 | }, 31 | } 32 | }, 33 | title: { 34 | display: true, 35 | text: 'Rows Per Table', 36 | color: theme === "light" ? '#17012866' : '#ffffffac', 37 | font: { 38 | size: 14 39 | } 40 | }, 41 | }, 42 | }; 43 | 44 | 45 | const data = { 46 | labels: rows.map(table => table.tableName), 47 | datasets: [ 48 | { 49 | label: 'Rows Per Table', 50 | data: rows.map(table => table.numberOfRows), 51 | backgroundColor: [ 52 | 'rgba(190, 99, 255, 0.2)', 53 | 'rgba(54, 162, 235, 0.2)', 54 | 'rgba(235, 86, 255, 0.2)', 55 | 'rgba(16, 39, 215, 0.2)', 56 | 'rgba(129, 75, 236, 0.2)', 57 | 'rgba(64, 118, 255, 0.2)', 58 | ], 59 | borderColor: [ 60 | '#dbdbdbdf', 61 | // "rgba(54, 162, 235, 1)", 62 | // "rgba(255, 206, 86, 1)", 63 | // "rgba(75, 192, 192, 1)", 64 | // "rgba(153, 102, 255, 1)", 65 | // "rgba(255, 159, 64, 1)", 66 | ], 67 | borderWidth: 1, 68 | } 69 | ] 70 | }; 71 | return ( 72 |
73 | 74 |
75 | ); 76 | }; 77 | 78 | // To pass data from the `MetricsView` component to the `PieChart` component using TypeScript, you'll need to ensure that: 79 | 80 | // 1. The `PieChart` component is correctly set up to receive the necessary props. 81 | // 2. The `MetricsView` component is passing the data as props to the `PieChart` component. 82 | // 3. TypeScript types are correctly set up for these props for type-safety. 83 | 84 | // Let's break this down step by step: 85 | 86 | // ### 1. Define the Prop Types for PieChart: 87 | 88 | // From the code you provided, it looks like you're trying to pass `tableInfo` as a prop to `PieChart`. 89 | 90 | // In `PieChart`, you hinted at a type `rowsInfo` for this prop, but didn't provide its structure. Let's define this type: 91 | 92 | // ```typescript 93 | // // Assuming this is in types.ts or similar 94 | // export type TableInfo = { 95 | // tableName: string; 96 | // numberOfRows: number; 97 | // }; 98 | 99 | // export type rowsInfo = TableInfo[]; 100 | // ``` 101 | 102 | // ### 2. Update PieChart to Use the Prop Types: 103 | 104 | // Now, in your `PieChart` component, you should define and use these prop types: 105 | 106 | // ```typescript 107 | // interface PieChartProps { 108 | // tableInfo: rowsInfo; 109 | // } 110 | 111 | // export const PieChart: React.FC = ({ tableInfo }) => { 112 | // // rest of the component... 113 | // }; 114 | // ``` 115 | 116 | // ### 3. Pass the Data from MetricsView to PieChart: 117 | 118 | // Now, in `MetricsView`, when you're creating the `PieChart` components, you can pass the `rowsData` as `tableInfo` prop: 119 | 120 | // ```typescript 121 | // const pieChartComponents = rowsData.map((rowData, index) => ( 122 | // 123 | // )); 124 | // ``` 125 | 126 | // Note: It's not ideal to use the index as the key for React lists, but if `tableName` is unique for each `rowData`, it would be better to use that: 127 | 128 | // ```typescript 129 | // const pieChartComponents = rowsData.map(rowData => ( 130 | // 131 | // )); 132 | // ``` 133 | 134 | // ### 4. Ensure Data Transformation is Correct: 135 | 136 | // The code you provided transforms `tablesArray` into `rowsData` and then tries to pass each item of `rowsData` to individual `PieChart` components. Ensure that the data structure and transformations are correct and fit the expected prop types. 137 | 138 | // ### Conclusion: 139 | 140 | // By defining prop types in TypeScript and passing data as props, you're ensuring type-safety and making sure that the components receive and use data as expected. This minimizes runtime errors and makes the codebase easier to understand and maintain. -------------------------------------------------------------------------------- /src/components/charts/SlowestCommonQueriesTop10.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | 4 | // type ExecTimeByOperation = { 5 | // [operation: string] : { 6 | // query: string; 7 | // operation: string; 8 | // median_exec_time: number; 9 | // mean_exec_time: number; 10 | // stdev_exec_time:number; 11 | // min_exec_time: number; 12 | // max_exec_time: number; 13 | // execution_count: number; 14 | // } 15 | // } 16 | 17 | // interface SlowQueryObj { 18 | // query:string; 19 | // median: number; 20 | // mean: number; 21 | // } 22 | 23 | // type mainArray = { 24 | // [queryName:string]:SlowQueryObj 25 | // } 26 | // const splitBySpaces: string[] = (inputStr: string, spaceCount: number) => { 27 | // let chunks = []; 28 | // let parts = inputStr.split(' '); 29 | 30 | // while (parts.length) { 31 | // chunks.push(parts.splice(0, spaceCount).join(' ')); 32 | // } 33 | 34 | // return chunks; 35 | // }; 36 | const splitByLength: any = (inputStr:string, minLength:number, maxLength:number) => { 37 | const parts = inputStr.split(' '); 38 | let chunks = []; 39 | let chunk = ""; 40 | 41 | for (let part of parts) { 42 | // If the current chunk plus the next word is within the limit, add the word to the chunk. 43 | if (chunk.length + part.length <= maxLength) { 44 | chunk += (chunk ? " " : "") + part; 45 | } else { 46 | // If it exceeds, push the current chunk to the chunks array and reset the chunk. 47 | chunks.push(chunk); 48 | chunk = part; 49 | } 50 | } 51 | 52 | // Ensure that the last chunk doesn't fall below the minimum length. 53 | // If it does, merge it with the previous chunk. 54 | if (chunks.length && (chunks[chunks.length - 1].length + chunk.length) < minLength) { 55 | chunks[chunks.length - 1] += " " + chunk; 56 | } else { 57 | chunks.push(chunk); 58 | } 59 | 60 | return chunks.filter(chunk => chunk.length >= minLength); 61 | }; 62 | 63 | 64 | export const SlowestCommonQueriesTop10: React.FC = () => { 65 | const { metricsData, theme } = useAppStore(); 66 | console.log(metricsData); 67 | 68 | const shortLabelsArr : string[] = []; 69 | const longLabelsArr : string[] = []; 70 | const meanArr : number[] = []; 71 | const medianArr : number[] = []; 72 | const countArr : number[] = []; 73 | 74 | let count = 0; 75 | for (const query in metricsData.dbHistMetrics.slowestCommonQueries) { 76 | if (count >= 10) break; // Limit to top 10 77 | shortLabelsArr.push(query); 78 | longLabelsArr.push(metricsData.dbHistMetrics.slowestCommonQueries[query].query); 79 | meanArr.push(metricsData.dbHistMetrics.slowestCommonQueries[query].mean); 80 | medianArr.push(metricsData.dbHistMetrics.slowestCommonQueries[query].median); 81 | countArr.push(metricsData.dbHistMetrics.slowestCommonQueries[query].count); 82 | count++; 83 | } 84 | //tooltip function 85 | // const footer = (tooltipItems) => { 86 | // let sum = 0; 87 | 88 | // tooltipItems.forEach(function(tooltipItem) { 89 | // sum += 1; 90 | // // sum += tooltipItem.parsed.y; 91 | // }); 92 | // return 'Sum: ' + sum; 93 | // }; 94 | const options: any = { 95 | indexAxis: 'y', 96 | responsive: true, 97 | maintainAspectRatio: false, 98 | 99 | plugins: { 100 | tooltip: { 101 | // enabled: false, // disable default tooltips 102 | // backgroundColor: 'rgb(255, 0, 200)', 103 | // titleColor:'rgb(0,0,255)', 104 | padding: { 105 | left: 3, 106 | right: 3, 107 | top: 2, 108 | bottom: 2 109 | }, 110 | bodyFont: { 111 | size: 8 // adjust as needed 112 | }, 113 | titleFont: { 114 | size: 10 // adjust as needed 115 | }, 116 | // displayColors: false, 117 | callbacks:{ 118 | afterLabel: function(context: any) { 119 | // Assuming that execution count is stored in an array 120 | const execCount = countArr[context.dataIndex]; 121 | let queryString = longLabelsArr[context.dataIndex]; 122 | let wrappedStringArray = splitByLength(queryString, 20,40); 123 | 124 | return [ 125 | 'Exec Count: ' + execCount, 126 | 'Query: ', ...wrappedStringArray 127 | ]; 128 | }, 129 | }, 130 | }, 131 | title: { 132 | // position: 'top' as const, // Position title at the top 133 | display: true, 134 | text: 'Top 10 Slowest Common Executed Queries Ordered By Exec. Count', 135 | color: theme === "light" ? '#17012866' : '#ffffffac', 136 | font: { 137 | size: 14 138 | }, 139 | }, 140 | legend: { 141 | display: true, 142 | position: 'bottom' as const, 143 | labels:{ 144 | font: { 145 | size: 12 146 | }, 147 | }, 148 | }, 149 | }, 150 | scales: { 151 | y: { 152 | beginAtZero: true, 153 | } 154 | }, 155 | 156 | 157 | }; 158 | const data = { 159 | labels: shortLabelsArr, 160 | datasets: [ 161 | { 162 | label: 'Mean Exec Time (ms)', 163 | data: meanArr.map((el)=> {return (el * 1000);}), 164 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 165 | scaleFontColor: '#FFFFFF', 166 | }, 167 | // { 168 | // label: 'Median Exec Time (ms)', 169 | // data: medianArr, 170 | // backgroundColor: 'rgba(53, 162, 235, 0.5)', 171 | // scaleFontColor: '#FFFFFF', 172 | // }, 173 | ], 174 | }; 175 | return ( 176 |
177 | 178 |
179 | ); 180 | }; -------------------------------------------------------------------------------- /src/components/charts/SlowestQueriesTop10.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | 4 | // type ExecTimeByOperation = { 5 | // [operation: string] : { 6 | // query: string; 7 | // operation: string; 8 | // median_exec_time: number; 9 | // mean_exec_time: number; 10 | // stdev_exec_time:number; 11 | // min_exec_time: number; 12 | // max_exec_time: number; 13 | // execution_count: number; 14 | // } 15 | // } 16 | 17 | // interface SlowQueryObj { 18 | // query:string; 19 | // median: number; 20 | // mean: number; 21 | // } 22 | 23 | // type mainArray = { 24 | // [queryName:string]:SlowQueryObj 25 | // } 26 | 27 | // const splitBySpaces: any = (inputStr: string, spaceCount: number) => { 28 | // let chunks = []; 29 | // let parts = inputStr.split(' '); 30 | 31 | // while (parts.length) { 32 | // chunks.push(parts.splice(0, spaceCount).join(' ')); 33 | // } 34 | 35 | // return chunks; 36 | // }; 37 | 38 | const splitByLength: any = (inputStr:string, minLength:number, maxLength:number) => { 39 | const parts = inputStr.split(' '); 40 | let chunks = []; 41 | let chunk = ""; 42 | 43 | for (let part of parts) { 44 | // If the current chunk plus the next word is within the limit, add the word to the chunk. 45 | if (chunk.length + part.length <= maxLength) { 46 | chunk += (chunk ? " " : "") + part; 47 | } else { 48 | // If it exceeds, push the current chunk to the chunks array and reset the chunk. 49 | chunks.push(chunk); 50 | chunk = part; 51 | } 52 | } 53 | 54 | // Ensure that the last chunk doesn't fall below the minimum length. 55 | // If it does, merge it with the previous chunk. 56 | if (chunks.length && (chunks[chunks.length - 1].length + chunk.length) < minLength) { 57 | chunks[chunks.length - 1] += " " + chunk; 58 | } else { 59 | chunks.push(chunk); 60 | } 61 | 62 | return chunks.filter(chunk => chunk.length >= minLength); 63 | }; 64 | 65 | export const SlowestQueriesTop10: React.FC = () => { 66 | const { metricsData, theme } = useAppStore(); 67 | const shortLabelsArr : string[] = []; 68 | const longLabelsArr : string[] = []; 69 | const meanArr : number[] = []; 70 | const medianArr : number[] = []; 71 | 72 | let count = 0; 73 | for (const query in metricsData.dbHistMetrics.slowestTotalQueries) { 74 | if (count >= 10) break; // Limit to top 10 75 | shortLabelsArr.push(query); 76 | longLabelsArr.push(metricsData.dbHistMetrics.slowestTotalQueries[query].query); 77 | meanArr.push(metricsData.dbHistMetrics.slowestTotalQueries[query].mean); 78 | medianArr.push(metricsData.dbHistMetrics.slowestTotalQueries[query].median); 79 | 80 | count++; 81 | } 82 | 83 | const options: any = { 84 | indexAxis: 'y', 85 | responsive: true, 86 | maintainAspectRatio: false, 87 | // layout: { 88 | // padding: { 89 | // top: 0, // Adjust the padding top value to create space for the title 90 | // bottom: 0, 91 | // }, 92 | // }, 93 | plugins: { 94 | title: { 95 | // position: 'top' as const, // Position title at the top 96 | display: true, 97 | text: 'Top 10 Slowest Previously Executed Queries', 98 | color: theme === "light" ? '#17012866' : '#ffffffac', 99 | font: { 100 | size: 14 101 | }, 102 | 103 | }, 104 | legend: { 105 | display: true, 106 | position: 'bottom' as const, 107 | labels:{ 108 | font: { 109 | size: 12, 110 | }, 111 | }, 112 | }, 113 | tooltip: { 114 | padding: { 115 | left: 3, 116 | right: 3, 117 | top: 2, 118 | bottom: 2 119 | }, 120 | bodyFont: { 121 | size: 7 122 | }, 123 | titleFont: { 124 | size: 10 125 | }, 126 | callbacks:{ 127 | afterLabel: function(context: any) { 128 | 129 | 130 | let queryString = longLabelsArr[context.dataIndex]; 131 | let wrappedStringArray = splitByLength(queryString, 25,40); 132 | 133 | return [ 134 | 'Query: ', ...wrappedStringArray 135 | ]; 136 | }, 137 | }, 138 | }, 139 | }, 140 | scales: { 141 | y: { 142 | beginAtZero: true, 143 | } 144 | }, 145 | }; 146 | 147 | const data = { 148 | labels: shortLabelsArr, 149 | datasets: [ 150 | { 151 | label: 'Mean Exec Time (ms)', 152 | data: meanArr.map((el)=> {return (el * 1000);}), 153 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 154 | scaleFontColor: '#FFFFFF', 155 | }, 156 | // { 157 | // label: 'Median Exec Time (ms)', 158 | // data: medianArr, 159 | // backgroundColor: 'rgba(53, 162, 235, 0.5)', 160 | // scaleFontColor: '#FFFFFF', 161 | // }, 162 | ], 163 | }; 164 | return ( 165 |
166 | 167 |
168 | ); 169 | }; -------------------------------------------------------------------------------- /src/components/charts/TableIndexSizes.tsx: -------------------------------------------------------------------------------- 1 | import useAppStore from '../../store/appStore'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import { IndexItem } from '../../Types'; 4 | import { ChartOptions } from 'chart.js'; 5 | 6 | export const TableIndexSizes: React.FC = () => { 7 | const { metricsData, toNumInKB, theme } = useAppStore(); 8 | const indexesArray: {}[] = Object.values(metricsData.dbSizeMetrics.indexSizesByTable); 9 | const indexSizeByTableArray: number[] = indexesArray.map((table: IndexItem) => { 10 | let total = 0; 11 | 12 | for (const key in table) { 13 | total += toNumInKB(table[key]); 14 | } 15 | return total; 16 | }); 17 | 18 | const options: ChartOptions = { 19 | indexAxis: 'y', 20 | responsive: true, 21 | maintainAspectRatio: false, 22 | plugins: { 23 | legend: { 24 | display: true, 25 | position: 'bottom' as const, 26 | labels: { 27 | font: { 28 | size: 12 29 | } 30 | } 31 | }, 32 | title: { 33 | display: true, 34 | text: 'Index Size by Table', 35 | color: theme === "light" ? '#17012866' : '#ffffffac', 36 | font: { 37 | size: 14 38 | } 39 | }, 40 | }, 41 | }; 42 | 43 | const data = { 44 | labels: metricsData.dbSizeMetrics.tableNames, 45 | datasets: [ 46 | { 47 | label: 'Index Size (kbs)', 48 | data: indexSizeByTableArray, 49 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 50 | scaleFontColor: '#FFFFFF', 51 | }, 52 | ] 53 | }; 54 | return ( 55 |
56 | 57 |
58 | ); 59 | }; -------------------------------------------------------------------------------- /src/components/charts/TableSize.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | import { ChartOptions } from 'chart.js'; 4 | 5 | interface Table { 6 | diskSize: string; 7 | rowSize: string; 8 | } 9 | 10 | 11 | export const TableSize: React.FC = () => { 12 | const { metricsData, toNumInKB, theme } = useAppStore(); 13 | console.log('metrics data',metricsData) 14 | const tablesArray: Table[] = Object.values(metricsData.dbSizeMetrics.tableSizes); 15 | 16 | const options: ChartOptions = { 17 | indexAxis: 'y', 18 | responsive: true, 19 | maintainAspectRatio: false, 20 | plugins: { 21 | title: { 22 | display: true, 23 | text: 'Table Sizes', 24 | color: theme === "light" ? '#17012866' : '#ffffffac', 25 | font: { 26 | size: 14 27 | } 28 | }, 29 | legend: { 30 | position: 'bottom' as const, 31 | labels:{ 32 | font: { 33 | size: 12, 34 | }, 35 | }, 36 | }, 37 | 38 | }, 39 | }; 40 | 41 | const data = { 42 | labels: Object.keys(metricsData.dbSizeMetrics.tableSizes), 43 | datasets: [ 44 | { 45 | label: 'Disk Size (kb)', 46 | data: tablesArray.map(table => toNumInKB(table.diskSize)), 47 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 48 | scaleFontColor: "#FFFFFF", 49 | }, 50 | { 51 | label: 'Row Size (kb)', 52 | data: tablesArray.map(table => toNumInKB(table.rowSize)), 53 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 54 | scaleFontColor: "#FFFFFF", 55 | }, 56 | ], 57 | }; 58 | return ( 59 | <> 60 |
61 | 62 |
63 | 64 | ); 65 | } -------------------------------------------------------------------------------- /src/components/charts/execTimeByOperation.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | 4 | // type ExecTimeByOperation = { 5 | // [operation: string] : { 6 | // query: string; 7 | // operation: string; 8 | // median_exec_time: number; 9 | // mean_exec_time: number; 10 | // stdev_exec_time:number; 11 | // min_exec_time: number; 12 | // max_exec_time: number; 13 | // execution_count: number; 14 | 15 | // } 16 | // } 17 | 18 | interface Table { 19 | query: string; 20 | operation: string; 21 | median_exec_time: number; 22 | mean_exec_time: number; 23 | stdev_exec_time:number; 24 | min_exec_time: number; 25 | max_exec_time: number; 26 | execution_count: number; 27 | } 28 | 29 | 30 | export const ExecTimesByOperation: React.FC = () => { 31 | const { metricsData, theme } = useAppStore(); 32 | console.log(metricsData); 33 | const tablesArray: Table[] = Object.values(metricsData.dbHistMetrics.execTimesByOperation); 34 | 35 | //CREATING the object for MinMax 36 | // const m; 37 | 38 | 39 | const options = { 40 | 41 | responsive: true, 42 | maintainAspectRatio: false, // This will allow the chart to stretch to fill its container 43 | // layout: { 44 | // padding: { 45 | // top: 0, // Adjust the padding top value to create space for the title 46 | // bottom: 0, 47 | // }, 48 | // }, 49 | plugins: { 50 | title: { 51 | display: true, 52 | text: 'Mean/Median Exec Times by Operations (All Queries)', 53 | color: theme === "light" ? '#17012866' : '#ffffffac', 54 | font: { 55 | size: 14 56 | }, 57 | 58 | }, 59 | legend: { 60 | display: true, 61 | position: 'bottom' as const, 62 | labels:{ 63 | font: { 64 | size: 12 65 | }, 66 | }, 67 | }, 68 | }, 69 | scales: { 70 | y: { 71 | beginAtZero: true, 72 | } 73 | } 74 | }; 75 | // const errorBarsData = tablesArray.map(object => ({ 76 | // y: object.mean_exec_time * 1000, 77 | // y: 5000, 78 | // min: object.min_exec_time * 1000, 79 | // max: object.max_exec_time * 1000 80 | // })); 81 | const data = { 82 | labels: Object.keys(metricsData.dbHistMetrics.execTimesByOperation), 83 | datasets: [ 84 | { 85 | label: 'Mean Exec Time (ms)', 86 | data: tablesArray.map(object => object.mean_exec_time ), 87 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 88 | scaleFontColor: '#FFFFFF', 89 | }, 90 | { 91 | label: 'Median Exec Time (ms)', 92 | data: tablesArray.map(object => object.median_exec_time), 93 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 94 | scaleFontColor: '#FFFFFF', 95 | }, 96 | // { 97 | // label: 'Error Bars', 98 | // data: errorBarsData, 99 | // type: 'line', 100 | // borderColor: 'red', 101 | // // borderWidth: 2, 102 | // // pointRadius: 0, 103 | // fill: false, 104 | // tension: 0, 105 | // yAxisID: 'Min/Max' 106 | // } 107 | ], 108 | }; 109 | return ( 110 |
111 | 112 |
113 | ); 114 | }; -------------------------------------------------------------------------------- /src/components/customQueryCharts/CustomQueryGeneralMetrics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useAppStore from "../../store/appStore"; 3 | import { CustomTableInfo } from '../../store/appStore' 4 | 5 | 6 | export const CustomQueryGeneralMetrics: React.FC = () => { 7 | const { customQueryData } = useAppStore(); 8 | const tablesObject: CustomTableInfo = { 9 | nodeType: customQueryData.nodeType, 10 | actualRows: customQueryData.actualRows, 11 | actualLoops: customQueryData.actualLoops, 12 | sharedHitBlocks: customQueryData.sharedHitBlocks, 13 | sharedReadBlocks: customQueryData.sharedReadBlocks, 14 | totalCosts: customQueryData.totalCosts, 15 | startUpCosts: customQueryData.startUpCosts, 16 | }; 17 | // const rows = tablesArray.map(table => { 18 | // return { 19 | // tableName: table.tableName, 20 | // numberOfForeignKeys: table.numberOfForeignKeys, 21 | // numberOfFields: table.numberOfFields, 22 | // } 23 | // }) 24 | 25 | return ( 26 | <> 27 |
28 |
29 |
Type of Scan
30 |

{tablesObject.nodeType}

31 |
32 |
33 |
34 |
35 |
Number of Rows
36 |

{tablesObject.actualRows}

37 |
Number of Loops
38 |

{tablesObject.actualLoops}

39 |
40 |
41 |
42 |
43 |
Shared Hit Blocks
44 |

{tablesObject.sharedHitBlocks}

45 |
Shared Read Blocks
46 |

{tablesObject.sharedReadBlocks}

47 |
48 |
49 |
50 |
51 |
Start Up Costs
52 |

{tablesObject.startUpCosts}

53 |
Total Costs
54 |

{tablesObject.totalCosts}

55 |
56 |
57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/customQueryCharts/MeanPlanningExecutionTimes.tsx: -------------------------------------------------------------------------------- 1 | import { Bar } from 'react-chartjs-2'; 2 | import useAppStore from '../../store/appStore'; 3 | 4 | 5 | export const MeanPlanningExecutionTimes: React.FC = () => { 6 | const { customQueryData, theme } = useAppStore(); 7 | 8 | const options = { 9 | responsive: true, 10 | maintainAspectRatio: false, 11 | layout: { 12 | padding: { 13 | top: 0, // Adjust the padding top value to create space for the title 14 | bottom: 0, 15 | }, 16 | }, 17 | plugins: { 18 | title: { 19 | position: 'top' as const, // Position title at the top 20 | display: true, 21 | text: 'Mean Exec & Planning Times (Among All 10 Queries)', 22 | color: theme === "light" ? '#17012866' : '#ffffffac', 23 | font: { 24 | size: 10 25 | }, 26 | 27 | }, 28 | // legend: { 29 | // display: true, 30 | // position: 'bottom' as const, 31 | // labels:{ 32 | // font: { 33 | // size: '10%', // Adjust the percentage value as needed 34 | // }, 35 | // }, 36 | // }, 37 | }, 38 | scales: { 39 | y: { 40 | beginAtZero: true, 41 | grid: { 42 | color: theme === "light" ? '#17012813' : '#ffffff1a' 43 | } 44 | }, 45 | x: { 46 | grid: { 47 | color: theme === "light" ? '#17012813' : '#ffffff1a' 48 | } 49 | } 50 | 51 | } 52 | }; 53 | // const errorBarsData = tablesArray.map(object => ({ 54 | // y: object.mean_exec_time * 1000, 55 | // y: 5000, 56 | // min: object.min_exec_time * 1000, 57 | // max: object.max_exec_time * 1000 58 | // })); 59 | const data = { 60 | labels: customQueryData.overallMeanTimesLabels, 61 | datasets: [ 62 | { 63 | label: 'Mean Exec Time (ms)', 64 | data: customQueryData.overallMeanTimesArr.map((time)=>{ 65 | return time * 1000; 66 | }), 67 | backgroundColor: [ 68 | theme === 'light'? 'rgba(190, 99, 255, 0.3)' : 'rgba(190, 99, 255, 0.5)', 69 | theme === 'light' ? 'rgba(54, 162, 235, 0.3)' : 'rgba(54, 163, 235, 0.5) ', 70 | theme === 'light' ? 'rgba(235, 86, 255, 0.3)' : 'rgba(235, 86, 255, 0.5)', 71 | ], 72 | scaleFontColor: '#FFFFFF', 73 | }, 74 | // { 75 | // label: 'Error Bars', 76 | // data: errorBarsData, 77 | // type: 'line', 78 | // borderColor: 'red', 79 | // // borderWidth: 2, 80 | // // pointRadius: 0, 81 | // fill: false, 82 | // tension: 0, 83 | // yAxisID: 'Min/Max' 84 | // } 85 | ], 86 | }; 87 | return ( 88 |
89 | 90 |
91 | ); 92 | }; -------------------------------------------------------------------------------- /src/components/customQueryCharts/PlanningExecutionTimes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useAppStore from '../../store/appStore'; 3 | // import { TableInfo } from '../../store/appStore'; 4 | import { Line } from 'react-chartjs-2'; 5 | 6 | 7 | export const PlanningExecutionTimes: React.FC = () => { 8 | const { customQueryData, theme } = useAppStore(); 9 | const labelsArr: number[] = customQueryData.labelsArr; 10 | const executionTimesArr: number[] = customQueryData.executionTimesArr.map((time) => time * 1000); 11 | const planningTimesArr: number[] = customQueryData.planningTimesArr.map((time)=> time *1000); 12 | const totalTimesArr: number[] = customQueryData.totalTimesArr.map((time) => time * 1000); 13 | 14 | const options = { 15 | responsive: true, 16 | maintainAspectRatio: false, // This will allow the chart to stretch to fill its container 17 | plugins: { 18 | title: { 19 | // position: 'top' as const, // Position title at the top 20 | display: true, 21 | text: `Planning vs. Execution Time for ${customQueryData.queryCount} runs and a ${customQueryData.queryDelay} sec. delay`, 22 | color: theme === "light" ? '#17012866' : '#ffffffac', 23 | }, 24 | }, 25 | scales: { 26 | y: { 27 | beginAtZero: true, 28 | grid: { 29 | color: theme === "light" ? '#17012813' : '#ffffff1a' 30 | } 31 | }, 32 | x: { 33 | grid: { 34 | color: theme === "light" ? '#17012813' : '#ffffff1a' 35 | } 36 | } 37 | 38 | } 39 | }; 40 | 41 | const data = { 42 | labels: labelsArr, 43 | 44 | datasets: [ 45 | { 46 | label: 'Execution Time (ms)', 47 | data: executionTimesArr, 48 | backgroundColor: 'rgba(107, 99, 255, 0.5)', 49 | scaleFontColor: '#FFFFFF', 50 | borderColor: theme === "light" ? 'rgba(107, 99, 255, 0.713)' : 'rgba(107, 99, 255, 0.713)', 51 | }, 52 | { 53 | label: 'Planning Time (ms)', 54 | data: planningTimesArr, 55 | backgroundColor: 'rgba(54, 162, 235, 0.2)', 56 | scaleFontColor: '#FFFFFF', 57 | borderColor: theme === "light" ? 'rgba(54, 163, 235, 0.635)' : 'rgba(54, 163, 235, 0.635)' 58 | }, 59 | { 60 | label: 'Total Time (ms)', 61 | data: totalTimesArr, 62 | backgroundColor: 'rgba(235, 86, 255, 0.2)', 63 | borderColor: theme === "light" ? 'rgba(218, 86, 255, 0.476)' : 'rgba(235, 86, 255, 0.506)' 64 | } 65 | ] 66 | }; 67 | 68 | return ( 69 | <> 70 | 71 | 72 | 73 | ); 74 | }; -------------------------------------------------------------------------------- /src/components/layout/ConnectDB.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Dialog from '@radix-ui/react-dialog'; 3 | import useAppStore from '../../store/appStore'; 4 | import { Cross2Icon } from '@radix-ui/react-icons'; 5 | 6 | const ConnectDB: React.FC = () => { 7 | 8 | const { uri, setUri, connectToDatabase, closeConnectDB, isConnectDBOpen, dbName, invalidURIMessage } = useAppStore(); 9 | 10 | const handleClick = (): void => { 11 | connectToDatabase(uri, dbName); 12 | } 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | Connect to Database 21 | 22 | 23 | Please enter your connection string. 24 | 25 |
26 | 29 | setUri(e.target.value)} 35 | /> 36 |
37 |
38 | {invalidURIMessage &&

Invalid URI

} 39 | 45 |
46 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | 63 | export default ConnectDB; 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/components/layout/CustomQueryView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormField from '../ui/QueryForm'; 3 | import { PlanningExecutionTimes } from '../customQueryCharts/PlanningExecutionTimes'; 4 | 5 | import { CustomQueryGeneralMetrics } from '../customQueryCharts/CustomQueryGeneralMetrics'; 6 | 7 | import { MeanPlanningExecutionTimes } from '../customQueryCharts/MeanPlanningExecutionTimes'; 8 | import useAppStore from '../../store/appStore'; 9 | 10 | 11 | 12 | const CustomQueryView: React.FC = () => { 13 | const { customQueryData, view } = useAppStore(); 14 | 15 | // need to find data to check for conditional rendering 16 | return ( 17 | <> 18 | 19 |
20 | {!customQueryData.labelsArr.length && view !== 'loading' && 21 |
22 |
25 | Enter a query to see results 26 |
27 |
28 | } 29 | {customQueryData.labelsArr.length > 0 && 30 | <> 31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 | } 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default CustomQueryView; 48 | -------------------------------------------------------------------------------- /src/components/layout/DashNav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from "../../assets/logo-horizontal-v2.png"; 3 | import logodark from "../../assets/logo-horizontal-v2-darkmode.png"; 4 | 5 | import DropdownMenuDemo from "../ui/DropdownMenu"; 6 | 7 | // import { IconButton } from '@radix-ui/react-button'; 8 | 9 | import HelpModal from '../ui/HelpModal'; 10 | 11 | // import { Flex } from '@radix-ui/themes'; 12 | 13 | import useAppStore from "../../store/appStore"; 14 | import DarkModeToggle from "../ui/DarkModeToggle"; 15 | 16 | const DashNav: React.FC = () => { 17 | const { theme, } = useAppStore(); 18 | 19 | return ( 20 |
21 |
22 |
23 | {theme === "light" ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 |
29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default DashNav; 40 | -------------------------------------------------------------------------------- /src/components/layout/ERDView.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Flow from '../ReactFlow/flow'; 3 | import React from 'react'; 4 | 5 | const ERDView: React.FC = () => { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | }; 12 | 13 | export default ERDView; -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useAppStore from '../../store/appStore'; 3 | import logo from "../../assets/logo-horizontal-v2.png"; 4 | import logodark from "../../assets/logo-horizontal-v2-darkmode.png"; 5 | 6 | 7 | const Footer: React.FC = () => { 8 | 9 | const { theme } = useAppStore(); 10 | 11 | return ( 12 |
13 |
14 |
15 |

16 | Navigation 17 |

18 | 30 |
31 |
32 | {theme === "light" ? ( 33 | 34 | ) : ( 35 | 36 | )} 37 |
38 |
39 |

Community

40 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |

© 2023 ScanQL | MIT License

59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | export default Footer -------------------------------------------------------------------------------- /src/components/layout/MetricsView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth0 } from '@auth0/auth0-react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { TableSize } from '../charts/TableSize'; 5 | import { TableIndexSizes } from '../charts/TableIndexSizes'; 6 | import { RowsPerTable } from '../charts/RowsPerTable'; 7 | import { IndexPerTable } from '../charts/IndexPerTable'; 8 | import { GeneralMetrics } from '../charts/GeneralMetrics'; 9 | import { QueryTimes } from '../charts/QueryTimes'; 10 | import { useEffect } from 'react'; 11 | import useAppStore from '../../store/appStore'; 12 | import { 13 | Chart as ChartJS, 14 | CategoryScale, 15 | LinearScale, 16 | PointElement, 17 | LineElement, 18 | Title, 19 | Tooltip, 20 | Legend, 21 | BarElement, 22 | } from 'chart.js'; 23 | import { ColumnIndexSizes } from '../charts/ColumnIndexSizes'; 24 | import { MetricsSeparator } from '../ui/MetricsSeparator'; 25 | import { ExecTimesByOperation } from '../charts/execTimeByOperation'; 26 | import { SlowestQueriesTop10 } from '../charts/SlowestQueriesTop10'; 27 | import { SlowestCommonQueriesTop10 } from '../charts/SlowestCommonQueriesTop10'; 28 | import { UpdateIcon } from '@radix-ui/react-icons'; 29 | import { Button } from '@radix-ui/themes'; 30 | import { DBSizeCards } from '../charts/DbSizeCards'; 31 | 32 | ChartJS.register( 33 | CategoryScale, 34 | LinearScale, 35 | PointElement, 36 | BarElement, 37 | LineElement, 38 | Title, 39 | Tooltip, 40 | Legend 41 | ); 42 | 43 | const MetricsView: React.FC = () => { 44 | const navigate = useNavigate(); 45 | const { isAuthenticated } = useAuth0(); 46 | 47 | useEffect(() => { 48 | if (!isAuthenticated) navigate('/'); 49 | }, [isAuthenticated]); 50 | 51 | const { metricsData, uri, connectToDatabase, dbName } = useAppStore(); 52 | 53 | const handleClick = (): void => { 54 | connectToDatabase(uri, dbName); 55 | }; 56 | 57 | const executionTableNames: string[] = Object.keys( 58 | metricsData.executionPlans 59 | ); 60 | 61 | const executionTimes = Object.values(metricsData.executionPlans).map( 62 | (table, i: number) => { 63 | // grab the correct data and pass as props to each component 64 | if(table.UPDATE.plan){ 65 | return ( 66 | 71 | ); 72 | } 73 | return null; 74 | } 75 | ); 76 | 77 | 78 | return ( 79 | <> 80 |
81 | 82 | 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | {executionTimes} 107 | 108 | ); 109 | }; 110 | 111 | export default MetricsView; 112 | -------------------------------------------------------------------------------- /src/components/layout/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useAuth0 } from '@auth0/auth0-react' 4 | import HomeDropdownMenuIcon from '../ui/HomeDropdownMenu'; 5 | import { GitHubLogoIcon} from '@radix-ui/react-icons'; 6 | import { IconButton } from '@radix-ui/themes'; 7 | 8 | import DarkModeToggle from '../ui/DarkModeToggle'; 9 | 10 | const NavBar: React.FC = () => { 11 | const { isAuthenticated, loginWithRedirect } = useAuth0(); 12 | 13 | return ( 14 |
15 |
16 | Home 17 | About 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {(isAuthenticated) ? : } 30 |
31 |
32 | ) 33 | } 34 | 35 | export default NavBar; 36 | -------------------------------------------------------------------------------- /src/components/ui/CustomQueryBox.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/components/ui/CustomQueryBox.tsx -------------------------------------------------------------------------------- /src/components/ui/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 2 | import * as Switch from "@radix-ui/react-switch"; 3 | import { useEffect, } from "react"; 4 | // import { useMediaQuery } from "react-responsive"; 5 | import useAppStore from "../../store/appStore"; 6 | 7 | const DarkModeToggle = () => { 8 | 9 | const { theme, toggleTheme } = useAppStore(); 10 | 11 | useEffect(() => { 12 | if (theme === 'dark') { 13 | document.body.classList.add('dark'); 14 | } else { 15 | document.body.classList.remove('dark'); 16 | } 17 | }, [theme]); 18 | 19 | 20 | 21 | return ( 22 |
23 | 27 | 28 | { 29 | theme === 'light' ? 30 | 31 | 35 | 36 | : 37 | 38 | 39 | 40 | 41 | } 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default DarkModeToggle; 49 | 50 | -------------------------------------------------------------------------------- /src/components/ui/DashboardCard.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ScanQL/4a078415cf37a819e821c701fd9c8ef0ffc88c17/src/components/ui/DashboardCard.tsx -------------------------------------------------------------------------------- /src/components/ui/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import useAppStore from '../../store/appStore'; 2 | import { useAuth0 } from "@auth0/auth0-react"; 3 | 4 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; 5 | import { 6 | HamburgerMenuIcon, 7 | RocketIcon, 8 | CircleBackslashIcon, 9 | } from '@radix-ui/react-icons'; 10 | 11 | 12 | const DropdownMenuIcon = () => { 13 | const { openConnectDB } = useAppStore(); 14 | 15 | const { logout } = useAuth0(); 16 | 17 | return ( 18 | 19 | 20 | 26 | 27 | 28 | 29 | 33 | openConnectDB()} className="group text-[13px] leading-none text-indigo-900 text-opacity-80 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-indigo-900 bg-opacity-80 data-[highlighted]:text-violet1 hamburger-menu-text"> 34 | Connect to Database 35 |
36 | 37 |
38 |
39 | logout({ logoutParams: { returnTo: window.location.origin } })} className="group text-[13px] leading-none text-indigo-900 text-opacity-80 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-indigo-900 bg-opacity-80 data-[highlighted]:text-violet1 hamburger-menu-text"> 40 | Logout{' '} 41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | 53 | export default DropdownMenuIcon; 54 | 55 | -------------------------------------------------------------------------------- /src/components/ui/HelpModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Dialog from '@radix-ui/react-dialog'; 3 | // import { Cross2Icon } from '@radix-ui/react-icons'; 4 | import { Cross1Icon, QuestionMarkCircledIcon } from '@radix-ui/react-icons'; 5 | // import { RowsPerTable } from '../charts/RowsPerTable'; 6 | 7 | const HelpModal: React.FC = () => { 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | How to Get Started 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 |

Connect Your Database

33 |

1. Click on the Dropdown icon on the top right of the page.

34 |

2. Click on the "Connect to Database" button.

35 |

3. Enter your Postgres URI string.

36 | 37 |

Custom Query

38 |

To generate custom query metrics enter a valid query.

39 |
    Ex. SELECT * FROM user_table
40 | 41 |

General Tips

42 |

Click on the Refresh Icon found in the dashboard in order for the most recent database information.

43 | 44 | 45 |
46 |
47 |
48 |
49 | ) 50 | } 51 | 52 | export default HelpModal; 53 | -------------------------------------------------------------------------------- /src/components/ui/HomeDropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; 3 | import { useAuth0 } from "@auth0/auth0-react"; 4 | 5 | import { 6 | HamburgerMenuIcon, 7 | BarChartIcon, 8 | CircleBackslashIcon, 9 | } from '@radix-ui/react-icons'; 10 | 11 | 12 | 13 | const HomeDropdownMenuIcon = () => { 14 | const navigate = useNavigate(); 15 | 16 | function handleNavigateDashboard() { 17 | navigate('/dashboard'); 18 | } 19 | 20 | const { logout } = useAuth0(); 21 | 22 | return ( 23 | 24 | 25 | 30 | 31 | 32 | 33 | 37 | 38 | Dashboard 39 | 40 |
41 | 42 |
43 |
44 | 45 | logout({ logoutParams: { returnTo: window.location.origin } })} className="group text-[13px] leading-none text-indigo-900 text-opacity-80 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-indigo-900 bg-opacity-80 data-[highlighted]:text-violet1 hamburger-menu-text"> 46 | Logout 47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | 60 | export default HomeDropdownMenuIcon; -------------------------------------------------------------------------------- /src/components/ui/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../../assets/logo-horizontal-v2.png'; 3 | 4 | const Loading: React.FC = () => { 5 | const [progress, setProgress] = React.useState(13); 6 | 7 | React.useEffect(() => { 8 | const timer = setTimeout(() => setProgress(66), 600); 9 | return () => clearTimeout(timer); 10 | }, [progress]); 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /src/components/ui/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useAuth0 } from '@auth0/auth0-react'; 3 | import { Button } from '@radix-ui/themes'; 4 | 5 | 6 | const Login: React.FC = () => { 7 | const { loginWithRedirect } = useAuth0(); 8 | return ; 9 | } 10 | 11 | export default Login; 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/ui/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import { useAuth0 } from "@auth0/auth0-react"; 2 | 3 | const LogoutButton = () => { 4 | const { logout } = useAuth0(); 5 | 6 | return ( 7 | 10 | ); 11 | }; 12 | 13 | export default LogoutButton; -------------------------------------------------------------------------------- /src/components/ui/MetricsSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Separator from '@radix-ui/react-separator'; 3 | 4 | interface MetricsSeparatorProps { 5 | title: string; 6 | } 7 | 8 | export const MetricsSeparator: React.FC = ({title}) => { 9 | return ( 10 |
11 |
{title}
12 | 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/components/ui/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Dialog from '@radix-ui/react-dialog'; 3 | // import { Cross2Icon } from '@radix-ui/react-icons'; 4 | import useAppStore from '../../store/appStore'; 5 | 6 | import { RowsPerTable } from '../charts/RowsPerTable'; 7 | 8 | const Modal: React.FC = () => { 9 | const { closeModal } = useAppStore(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | {/* whichever component is clicked on needs to passed on the click and displayed here */} 17 |
18 | 19 |
20 |
21 |
22 |
23 | ) 24 | } 25 | 26 | export default Modal; 27 | 28 | -------------------------------------------------------------------------------- /src/components/ui/QueryForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Form from '@radix-ui/react-form'; 3 | import * as Separator from '@radix-ui/react-separator'; 4 | import useAppStore from '../../store/appStore'; 5 | 6 | 7 | const FormField: React.FC = () => { 8 | 9 | const { uri, queryString, setQuery, sendCustomQuery} = useAppStore(); 10 | 11 | const handleFormSubmit = (event: React.FormEvent) => { 12 | // prevent the form from submitting and refreshing the page 13 | event.preventDefault(); 14 | // send the custom query to the backend 15 | sendCustomQuery(uri, queryString); 16 | //clear the query field 17 | setQuery(''); 18 | }; 19 | 20 | return ( 21 |
22 | 23 | 24 |
25 | 26 | Custom Query Testing 27 | 28 |

29 | 30 | 31 |

32 | 33 | {/*

Enter the query you would like to test:

*/} 34 |
35 |
36 | 37 |