├── .dockerignore
├── .eslintrc.json
├── .github
└── workflows
│ ├── dev-push.yml
│ ├── main-push.yml
│ └── other.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile-dev
├── Dockerrun.aws.json
├── README.md
├── __tests__
├── __mocks__
│ └── fileMock.ts
├── jest-setup.js
├── jest-teardown.js
├── supertests.js
└── test components
│ ├── dashboard.test.js
│ ├── login.test.js
│ └── signup.test.js
├── babel.config.js
├── client
├── App.tsx
├── assets
│ ├── connect-to-db.gif
│ ├── db-metrics.gif
│ ├── dbhive-logo.png
│ ├── dbhive_logo_transparent.png
│ ├── icons8-hexagon-office-16.png
│ ├── icons8-hexagon-office-32.png
│ ├── icons8-hexagon-office-96.png
│ ├── postgresql_elephant.png
│ ├── toggle-dbs.gif
│ └── yellow_hex.png
├── clientMode.ts
├── clientTypes.ts
├── components
│ ├── CollapseList.tsx
│ ├── ConnectDB.tsx
│ ├── DBTab.tsx
│ ├── Input.tsx
│ ├── LineGraphType1.tsx
│ ├── LineGraphType2.tsx
│ ├── MetricCard.tsx
│ ├── Navbar.tsx
│ └── PieGraphType1.tsx
├── import-images.d.ts
├── index.html
├── index.tsx
├── pages
│ ├── Dashboard.tsx
│ ├── Docs.tsx
│ ├── Login.tsx
│ ├── Setup.tsx
│ └── Signup.tsx
├── store
│ ├── appStore.ts
│ └── rqHooks.ts
└── styles.css
├── db-sample-data
├── adventureworks.readme
├── csv-data.zip
├── erd.pgerd
├── install.sql
└── update_csvs.rb
├── dist
└── client
│ ├── App.d.ts
│ ├── clientMode.d.ts
│ ├── clientTypes.d.ts
│ ├── components
│ ├── CollapseList.d.ts
│ ├── ConnectDB.d.ts
│ ├── DBTab.d.ts
│ ├── DropdownMenu.d.ts
│ ├── ErrorBoundary.d.ts
│ ├── Graph1.d.ts
│ ├── Graph2.d.ts
│ ├── Input.d.ts
│ ├── LineGraphType1.d.ts
│ ├── LineGraphType2.d.ts
│ ├── MetricCard.d.ts
│ ├── Navbar.d.ts
│ └── PieGraphType1.d.ts
│ ├── index.d.ts
│ ├── pages
│ ├── Dashboard.d.ts
│ ├── Docs.d.ts
│ ├── Home.d.ts
│ ├── Login.d.ts
│ ├── Setup.d.ts
│ └── Signup.d.ts
│ └── store
│ ├── appStore.d.ts
│ └── rqHooks.d.ts
├── docker-compose-dev-hot.yml
├── docker-compose-test.yml
├── jest.config.js
├── package-lock.json
├── package.json
├── server
├── controllers
│ ├── connectController.ts
│ └── databaseController.ts
├── routes
│ └── metricApi.ts
└── server.ts
├── tsconfig.json
└── webpack.config.cjs
/.dockerignore:
--------------------------------------------------------------------------------
1 | db-sample-data
2 | __tests__
3 | node_modules
4 | README.md
5 | .elasticbeanstalk/*
6 | .git
7 | .gitignore
8 | .env
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:import/recommended",
12 | "plugin:import/typescript"
13 | ],
14 | "parser": "@typescript-eslint/parser",
15 |
16 | "settings": {
17 | "import/resolver": {
18 | "typescript": {},
19 | "node": {
20 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
21 | }
22 | }
23 | }
24 |
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/.github/workflows/dev-push.yml:
--------------------------------------------------------------------------------
1 | name: dev-push
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'dev'
7 |
8 | env:
9 | PIPELINE_ID: ${{ github.run_id }}-${{ github.run_number }}
10 | TEST_TAG: dbhive/main-prod:test
11 | LATEST_TAG: dbhive/main-prod:latest
12 | TEST_DB: ${{ secrets.TEST_DB }}
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v2
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v2
26 |
27 | - name: Login to Docker Hub
28 | uses: docker/login-action@v2
29 | with:
30 | username: ${{ secrets.DOCKERHUB_USERNAME }}
31 | password: ${{ secrets.DOCKERHUB_TOKEN }}
32 |
33 | - name: Configure AWS credentials
34 | uses: aws-actions/configure-aws-credentials@v1
35 | with:
36 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
37 | aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
38 | aws-region: us-west-2
39 |
40 | - name: Login to Amazon ECR
41 | id: login-ecr
42 | uses: aws-actions/amazon-ecr-login@v1
43 |
44 | - name: Build and export to Docker
45 | uses: docker/build-push-action@v4
46 | with:
47 | context: .
48 | load: true
49 | tags: ${{ env.TEST_TAG }}
50 |
51 | - name: Test
52 | run: docker-compose -f docker-compose-test.yml up --exit-code-from test
53 |
54 | - name: Build and push to DockerHub and ECR
55 | uses: docker/build-push-action@v4
56 | with:
57 | context: .
58 | # platforms: linux/amd64,linux/arm64
59 | push: true
60 | tags: |
61 | ${{ env.LATEST_TAG }}
62 | ${{ steps.login-ecr.outputs.registry }}/dbhive:main-prod
63 |
64 | deploy:
65 | needs: build
66 | runs-on: ubuntu-latest
67 | environment: your-github-environment
68 | steps:
69 | - name: Check out code
70 | uses: actions/checkout@v3
71 |
72 | - name: Deploy to AWS Elastic BeansTalk
73 | uses: einaregilsson/beanstalk-deploy@v20
74 | with:
75 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
76 | aws_secret_key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
77 | application_name: dbhive-gha
78 | environment_name: dbhive-gha
79 | version_label: ${{ env.PIPELINE_ID }}
80 | use_existing_version_if_available: true
81 | region: us-west-2
82 | deployment_package: Dockerrun.aws.json
83 |
--------------------------------------------------------------------------------
/.github/workflows/main-push.yml:
--------------------------------------------------------------------------------
1 | name: main-push
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 |
8 | env:
9 | PIPELINE_ID: ${{ github.run_id }}-${{ github.run_number }}
10 | TEST_TAG: dbhive/main-prod:test
11 | LATEST_TAG: dbhive/main-prod:latest
12 | TEST_DB: ${{ secrets.TEST_DB }}
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v2
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v2
26 |
27 | - name: Login to Docker Hub
28 | uses: docker/login-action@v2
29 | with:
30 | username: ${{ secrets.DOCKERHUB_USERNAME }}
31 | password: ${{ secrets.DOCKERHUB_TOKEN }}
32 |
33 | - name: Configure AWS credentials
34 | uses: aws-actions/configure-aws-credentials@v1
35 | with:
36 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
37 | aws-secret-access-key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
38 | aws-region: us-west-2
39 |
40 | - name: Login to Amazon ECR
41 | id: login-ecr
42 | uses: aws-actions/amazon-ecr-login@v1
43 |
44 | - name: Build and export to Docker
45 | uses: docker/build-push-action@v4
46 | with:
47 | context: .
48 | load: true
49 | tags: ${{ env.TEST_TAG }}
50 |
51 | - name: Test
52 | run: docker-compose -f docker-compose-test.yml up --exit-code-from test
53 |
54 | - name: Build and push to DockerHub and ECR
55 | uses: docker/build-push-action@v4
56 | with:
57 | context: .
58 | # platforms: linux/amd64,linux/arm64
59 | push: true
60 | tags: |
61 | ${{ env.LATEST_TAG }}
62 | ${{ steps.login-ecr.outputs.registry }}/dbhive:main-prod
63 |
64 | deploy:
65 | needs: build
66 | runs-on: ubuntu-latest
67 | environment: your-github-environment
68 | steps:
69 | - name: Check out code
70 | uses: actions/checkout@v3
71 |
72 | - name: Deploy to AWS Elastic BeansTalk
73 | uses: einaregilsson/beanstalk-deploy@v20
74 | with:
75 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
76 | aws_secret_key: ${{ secrets.AWS_ACCESS_KEY_SECRET }}
77 | application_name: dbhive-prod2
78 | environment_name: dbhive-prod2
79 | version_label: ${{ env.PIPELINE_ID }}
80 | use_existing_version_if_available: true
81 | region: us-west-2
82 | deployment_package: Dockerrun.aws.json
83 |
--------------------------------------------------------------------------------
/.github/workflows/other.yml:
--------------------------------------------------------------------------------
1 | name: other
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches-ignore:
9 | - 'main'
10 | - 'dev'
11 |
12 | env:
13 | TEST_TAG: dbhive/main-prod:test
14 | TEST_DB: ${{ secrets.TEST_DB }}
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 |
23 | - name: Set up QEMU
24 | uses: docker/setup-qemu-action@v2
25 |
26 | - name: Set up Docker Buildx
27 | uses: docker/setup-buildx-action@v2
28 |
29 | - name: Build and export to Docker
30 | uses: docker/build-push-action@v4
31 | with:
32 | context: .
33 | load: true
34 | tags: ${{ env.TEST_TAG }}
35 |
36 | - name: Test
37 | run: docker-compose -f docker-compose-test.yml up --exit-code-from test
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .DS_Store
4 | build/*
5 | # Elastic Beanstalk Files
6 | .elasticbeanstalk/*
7 | !.elasticbeanstalk/*.cfg.yml
8 | !.elasticbeanstalk/*.global.yml
9 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to dbHive
2 |
3 | Thank you for contributing!
4 |
5 | ## Pull Requests
6 |
7 | All code changes happen through Github pull requests. To submit your pull request, please follow the steps below:
8 |
9 | 1. Fork and clone the repository.
10 | 2. Create your feature branch. (git checkout -b [my-new-feature])
11 | 3. Make sure to cover your code with tests and that your code is linted in accordance with our linting specifications (see coding style below).
12 | 4. Commit your changes locally (git commit -m 'Added some feature') and then push to your remote repository.
13 | 5. Submit a pull request to the _dev_ branch, including in your pull request a description of the work done.
14 |
15 | ## Reporting Bugs
16 |
17 | We use GitHub Issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue.
18 |
19 | ## License
20 |
21 | By contributing, you agree that your contributions will be licensed under dbHive's MIT License.
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.13
2 | WORKDIR /usr/src/app
3 | COPY . .
4 | RUN npm ci && npm cache clean --force && npm run build
5 | EXPOSE 3000
6 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/Dockerfile-dev:
--------------------------------------------------------------------------------
1 | FROM node:alpine
2 | RUN npm i -g webpack
3 | WORKDIR /usr/src/app
4 | COPY package\*.json .
5 | RUN npm ci && npm cache clean --force
6 | EXPOSE 3000
7 |
--------------------------------------------------------------------------------
/Dockerrun.aws.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSEBDockerrunVersion": "1",
3 | "Image": {
4 | "Name": "096169716422.dkr.ecr.us-west-2.amazonaws.com/dbhive:main-prod",
5 | "Update": "true"
6 | },
7 | "Ports": [
8 | {
9 | "ContainerPort": "3000"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome to dbHive! 🐝
6 | PostgreSQL Monitoring Tool
7 |
8 |
9 | # Table of Contents
10 |
11 | - [About](#about)
12 | - [Getting Started](#getting-started)
13 | - [Features](#features)
14 | - [Troubleshooting](#troubleshooting)
15 | - [Contributing](#contributing)
16 |
17 | # About
18 |
19 | dbHive offers an interactive dashboard to visualize the performance of one or more PostgreSQL databases. By providing easily accessible information about the health and activity of a database, dbHive enables developers to make informed decisions that optimize the way they store their data.
20 |
21 | # Getting Started
22 |
23 | ### https://dbhive.net/
24 |
25 | # Features
26 |
27 | ### ➮ Query Execution Times
28 |
29 | Get a broad sense of database performance by viewing average query times across the database, as well as averages for specific types of SQL queries. Pinpoint the slowest queries to gain insight for database improvement.
30 |
31 | ###
32 |
33 | 
34 |
35 | ### ➮ Most Frequent Queries
36 |
37 | To analyze common and recurring database activity, view charts on the most frequent queries within and across all query types.
38 |
39 | ### ➮ Other Key Stats
40 |
41 | At a glance, gather other vital information, including:
42 |
43 | - conflicts
44 | - deadlocks
45 | - rolled back transactions
46 | - cache hit ratio
47 | - block hits
48 | - and more
49 |
50 | ### ➮ Access everything with a secure account
51 |
52 | Maintain privacy and security with dbHive's required secure login. All database information and metrics are protected with encrypted accounts.
53 |
54 | ### ➮ Connect one or more databases
55 |
56 | Easily access all databases and metrics within one place with the ability to toggle between multiple database dashboards.
57 |
58 | ###
59 |
60 | 
61 |
62 | ### ➮ Customize database metrics dashboard
63 |
64 | Use dropdowns to view more details on a given metric. Expand graphs to fill the screen. Adjust the fetch interval, the frequency at which the dashboard is updated with the latest metrics.
65 |
66 | ### ➮ Delete a database
67 |
68 | Navigate to the Setup page and remove a database when it is no longer in use, keeping your dashboard clean.
69 |
70 | # Privacy and Security
71 |
72 | dbHive does not store any user data, most importantly including database connection information, usernames, and passwords. Data is kept encrypted on the client.
73 |
74 | # Troubleshooting
75 |
76 | If certain database metrics are shown as unavailable in the dashboard, database user permissions may need to be elevated. Necessary user permissions and admin privileges can vary depending on the database hosting service used.
77 | For issues with application accounts, it is recommended to remove problematic users. Follow the directions in the "Remove application users" section of this readme.
78 |
79 | ### ➮ Shared library
80 |
81 | If you receive this error, 'error: pg_stat_statements must be loaded via shared_preload_libraries' , in **postgresql.conf** change
82 |
83 | ```
84 | shared_preload_libraries = ''
85 | ```
86 |
87 | to
88 |
89 | ```
90 | shared_preload_libraries = 'pg_stat_statements'
91 | ```
92 |
93 | and restart the Postgres service.
94 |
95 | # Contributing
96 |
97 | Read our [contribution guide](https://github.com/oslabs-beta/dbhive/blob/main/CONTRIBUTING.md) for more information on how to contribute to dbHive.
98 |
99 | ### Development Mode
100 |
101 | If you would like to participate in developing new features, the app can be launched in development mode:
102 |
103 | ```
104 | npm run dev
105 | ```
106 |
107 | or
108 |
109 | ```
110 | docker build -t dbhive/main-dev -f Dockerfile-dev .
111 | ```
112 |
113 | ```
114 | docker-compose -f docker-compose-dev-hot.yml up
115 | ```
116 |
117 | # Future Enhancements
118 |
119 | - **Search Feature:** Add a search bar to the dashboard that allows users to find data by keywords.
120 | - **Expansion to Other Databases:** Make dbHive available for other databases besides PostgreSQL.
121 | - **Comparing Schemas:** Allow users to compare the performance of alternate database schemas alongside their current schemas.
122 | - **Additional Customization:** Give users more power to customize graphs and dashboard arrangement.
123 |
124 | ### Authors
125 |
126 | - Melanie Forbes [GitHub](https://github.com/mforbz12) | [LinkedIn](https://www.linkedin.com/in/melanie-forbes-/)
127 | - Elise McConnell [GitHub](https://github.com/enmcco) | [LinkedIn](https://www.linkedin.com/in/elisemcconnell/)
128 | - Brandon Miller [GitHub](https://github.com/bmiller1881) | [LinkedIn](https://www.linkedin.com/in/brandon-j-miller/)
129 | - Emily Paine [GitHub](https://github.com/erpaine) | [LinkedIn](https://www.linkedin.com/in/emily-paine1/)
130 | - Jeffery Richardson [GitHub](https://github.com/jrichardson-rn) | [LinkedIn](https://www.linkedin.com/in/jeffery-richardson-ii-2ba819100/)
131 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/fileMock.ts:
--------------------------------------------------------------------------------
1 | module.exports = {}
--------------------------------------------------------------------------------
/__tests__/jest-setup.js:
--------------------------------------------------------------------------------
1 | //spin up rds instance (if no charge) add to read me how to run tests on client db
2 | // eslint-disable-next-line @typescript-eslint/no-var-requires
3 | const regeneratorRuntime = require('regenerator-runtime');
4 |
5 | module.exports = () => {
6 | console.log('Test db setup');
7 | global.testServer = require('../server/server.ts');
8 | };
9 |
--------------------------------------------------------------------------------
/__tests__/jest-teardown.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | module.exports = async (globalConfig) => {
3 | console.log('Closing down test db');
4 | testServer.close();
5 | };
6 |
--------------------------------------------------------------------------------
/__tests__/supertests.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const supertest = require('supertest');
3 | // eslint-disable-next-line @typescript-eslint/no-var-requires
4 | import { describe, it, expect, xdescribe, test } from '@jest/globals';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-var-requires
7 | const pg = require('pg');
8 |
9 | //Notice: dbtest_url is not a valid url and should be replaced with test db
10 | const dbtest_url = process.env.TEST_DB;
11 | const server = 'http://localhost:3000';
12 | const pool = new pg.Pool({
13 | connectionString: dbtest_url,
14 | });
15 |
16 | //testing the test config and setup
17 | describe('my test', () => {
18 | //test will pass
19 | test('passes', () => {
20 | expect(2).toEqual(2);
21 | });
22 | //test will fail
23 | test('fails', () => {
24 | expect(3).not.toEqual(2);
25 | });
26 | });
27 |
28 | describe('Connecting a database', () => {
29 | describe('connecting a URI key', () => {
30 | it('responds with a true valid URI key value', () => {
31 | const body = {
32 | uri: dbtest_url,
33 | };
34 | return supertest(server)
35 | .post('/api/uri')
36 | .send(body)
37 | .expect(200)
38 | .expect((res) => {
39 | expect(res.body.result.validURI).toBeTruthy();
40 | });
41 | });
42 | });
43 | });
44 |
45 | describe('Database data retrieval', () => {
46 | describe('connecting a valid URI key', () => {
47 | //a valid response would be an object containing lots of data to be rendered
48 | it('responds with a valid response', () => {
49 | const body = {
50 | uri: dbtest_url,
51 | };
52 | return supertest(server)
53 | .post('/api/queryMetrics')
54 | .send(body)
55 | .expect(200)
56 | .expect('Content-Type', /application\/json/)
57 | .expect((res) => {
58 | expect(typeof res).toEqual('object');
59 | });
60 | });
61 | //the response contains the keys that will be needed to render data
62 | it('responds with data in the expected keys', () => {
63 | const body = {
64 | uri: dbtest_url,
65 | };
66 | return supertest(server)
67 | .post('/api/queryMetrics')
68 | .send(body)
69 | .expect((res) => {
70 | expect(res.body.allTimes).toBeTruthy();
71 | expect(res.body.avgTimeTopAllCalls).toBeTruthy();
72 | expect(res.body.avgTimeTopDeleteCalls).toBeTruthy();
73 | expect(res.body.avgTimeTopInsertCalls).toBeTruthy();
74 | expect(res.body.avgTimeTopSelectCalls).toBeTruthy();
75 | expect(res.body.avgTimeTopUpdateCalls).toBeTruthy();
76 | expect(res.body.conflicts).toBeTruthy();
77 | expect(res.body.dbStats).toBeTruthy();
78 | expect(res.body.deadlocks).toBeTruthy();
79 | expect(res.body.deleteTimes).toBeTruthy();
80 | expect(res.body.insertTimes).toBeTruthy();
81 | expect(res.body.numOfRows).toBeTruthy();
82 | expect(res.body.rolledBackTransactions).toBeTruthy();
83 | expect(res.body.selectTimes).toBeTruthy();
84 | expect(res.body.transactionsCommitted).toBeTruthy();
85 | expect(res.body.updateTimes).toBeTruthy();
86 | expect(res.body.cacheHitRatio).toBeTruthy();
87 | });
88 | });
89 | });
90 | xdescribe('connecting an invalid URI key', () => {
91 | //an invalid key results in a bad request error
92 | it('responds with an error', () => {
93 | const body = {
94 | uri: 'postgres://xxxx:xxxxx@xxxxx.hostname.com:5432/databasename',
95 | };
96 | return supertest(server).post('/api/queryMetrics').send(body).expect(400);
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/__tests__/test components/dashboard.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Dashboard from '../../client/pages/Dashboard';
5 | import {
6 | describe,
7 | it,
8 | expect,
9 | xdescribe,
10 | beforeEach,
11 | beforeAll,
12 | test,
13 | } from '@jest/globals';
14 |
15 | describe('Dashboard Page', () => {
16 | let dashboardComp;
17 | const props = {};
18 |
19 | beforeEach(async () => {
20 | const loginComp = await render(
21 |
22 |
23 |
24 | );
25 | });
26 | test('if no DB has been connected, button prompts connecting one', async () => {
27 | expect(screen.getByText('Please connect a database')).toBeInTheDocument();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/__tests__/test components/login.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Login from '../../client/pages/Login';
5 | import {
6 | describe,
7 | it,
8 | expect,
9 | xdescribe,
10 | beforeEach,
11 | beforeAll,
12 | test,
13 | } from '@jest/globals';
14 |
15 | describe('Login Page', () => {
16 | let loginComp;
17 | const props = {};
18 |
19 | beforeEach(async () => {
20 | const loginComp = await render(
21 |
22 |
23 |
24 | );
25 | });
26 | test('Submit button rendered', async () => {
27 | const buttons = await screen.getAllByRole('button', { name: 'Submit' });
28 | expect(buttons.length).toBe(1);
29 | });
30 | //this value is 2 because it inclues the navbar button and the button on the main component
31 | test('Sign Up button rendered', async () => {
32 | const buttons = await screen.getAllByRole('button', { name: 'Sign Up' });
33 | expect(buttons.length).toBe(2);
34 | });
35 | //makes sure inputs are available
36 | test('Username input field to be rendered', async () => {
37 | //only text input is available by textbox role.
38 | expect(
39 | screen.getByRole('textbox', { name: 'Username:' })
40 | ).toBeInTheDocument();
41 | });
42 | test('Login header', async () => {
43 | const header = await screen.getByTestId('login-header');
44 | expect(header).toBeInTheDocument();
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/__tests__/test components/signup.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Signup from '../../client/pages/Signup';
5 | import {
6 | describe,
7 | it,
8 | expect,
9 | xdescribe,
10 | beforeEach,
11 | test,
12 | } from '@jest/globals';
13 |
14 | describe('Signup Page', () => {
15 | let signupComp;
16 |
17 | beforeEach(async () => {
18 | const signupComp = await render(
19 |
20 |
21 |
22 | );
23 | });
24 |
25 | test('Submit button rendered', async () => {
26 | const buttons = await screen.getAllByRole('button', { name: 'Submit' });
27 | expect(buttons.length).toBe(1);
28 | });
29 |
30 | test('Username input rendered', () => {
31 | expect(
32 | screen.getByRole('textbox', { name: 'Username:' })
33 | ).toBeInTheDocument();
34 | });
35 |
36 | test('Signup header', async () => {
37 | const header = await screen.getByTestId('signup-header');
38 | expect(header).toBeInTheDocument();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {presets: ['@babel/preset-env', '@babel/preset-react']}
--------------------------------------------------------------------------------
/client/App.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { createBrowserRouter, RouterProvider } from 'react-router-dom';
4 | import { QueryClientProvider, QueryClient } from 'react-query';
5 | import { ReactQueryDevtools } from 'react-query/devtools';
6 | import { ThemeProvider, createTheme } from '@mui/material/styles';
7 | import CssBaseline from '@mui/material/CssBaseline';
8 |
9 | // import react components
10 | import Docs from './pages/Docs';
11 | import Dashboard from './pages/Dashboard';
12 | import Login from './pages/Login';
13 | import Signup from './pages/Signup';
14 | import Setup from './pages/Setup';
15 |
16 | // import utilities
17 | import useAppStore from './store/appStore';
18 | import { UserData } from './clientTypes';
19 | import { seedDBs } from './clientMode';
20 |
21 | // custom MUI theme
22 | const darkTheme = createTheme({
23 | palette: {
24 | mode: 'dark',
25 | primary: {
26 | main: '#ffd900',
27 | },
28 | secondary: {
29 | main: '#134e00',
30 | },
31 | },
32 | typography: {
33 | fontFamily: [
34 | '-apple-system',
35 | 'BlinkMacSystemFont',
36 | '"Segoe UI"',
37 | 'Roboto',
38 | '"Helvetica Neue"',
39 | 'Arial',
40 | 'sans-serif',
41 | '"Apple Color Emoji"',
42 | '"Segoe UI Emoji"',
43 | '"Segoe UI Symbol"',
44 | ].join(','),
45 | },
46 | });
47 |
48 | function App() {
49 | const queryClient = new QueryClient();
50 |
51 | // update user data with seed data if given
52 | const updateUserData = useAppStore((state) => state.updateUserData);
53 | const initialUserData: UserData = {
54 | decryption: 'isValid',
55 | dbs: seedDBs,
56 | };
57 | updateUserData(initialUserData);
58 |
59 | const router = createBrowserRouter([
60 | {
61 | path: '/',
62 | element: ,
63 | },
64 | {
65 | path: '/setup',
66 | element: ,
67 | },
68 | {
69 | path: '/login',
70 | element: ,
71 | },
72 | {
73 | path: '/signup',
74 | element: ,
75 | },
76 | {
77 | path: '/dashboard',
78 | element: ,
79 | },
80 | ]);
81 |
82 | return (
83 | <>
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | >
92 | );
93 | }
94 |
95 | export default App;
96 |
--------------------------------------------------------------------------------
/client/assets/connect-to-db.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/connect-to-db.gif
--------------------------------------------------------------------------------
/client/assets/db-metrics.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/db-metrics.gif
--------------------------------------------------------------------------------
/client/assets/dbhive-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/dbhive-logo.png
--------------------------------------------------------------------------------
/client/assets/dbhive_logo_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/dbhive_logo_transparent.png
--------------------------------------------------------------------------------
/client/assets/icons8-hexagon-office-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/icons8-hexagon-office-16.png
--------------------------------------------------------------------------------
/client/assets/icons8-hexagon-office-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/icons8-hexagon-office-32.png
--------------------------------------------------------------------------------
/client/assets/icons8-hexagon-office-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/icons8-hexagon-office-96.png
--------------------------------------------------------------------------------
/client/assets/postgresql_elephant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/postgresql_elephant.png
--------------------------------------------------------------------------------
/client/assets/toggle-dbs.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/toggle-dbs.gif
--------------------------------------------------------------------------------
/client/assets/yellow_hex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/client/assets/yellow_hex.png
--------------------------------------------------------------------------------
/client/clientMode.ts:
--------------------------------------------------------------------------------
1 | import { UserData } from './clientTypes';
2 |
3 | /*
4 | toggleDashboardAuth default = true, use for production
5 | toggleDashboardAuth = false, deactivates dashboard page authorization
6 | allowing navigation to the page without having to be logged in
7 | */
8 | const toggleDashboardAuth = true;
9 |
10 | /*
11 | seedDBs default = [], use for production
12 | Seed db data so that developers do not have to login to see data displayed on the dashboard
13 | below is the format for seedDBs:
14 | [
15 | {
16 | nickname: 'dbNickname',
17 | uri: 'postgres://username:passsord@hostname:5432/databaseName',
18 | },
19 | ]
20 | */
21 |
22 | const seedDBs: UserData['dbs'] = [];
23 |
24 | export { toggleDashboardAuth, seedDBs };
25 |
--------------------------------------------------------------------------------
/client/clientTypes.ts:
--------------------------------------------------------------------------------
1 | export type UserData = {
2 | decryption: string;
3 | dbs: { nickname: string; uri: string }[];
4 | };
5 |
6 | export type DbData = {
7 | nickname: string;
8 | uri: string;
9 | };
10 |
--------------------------------------------------------------------------------
/client/components/CollapseList.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import List from '@mui/material/List';
5 | import ListItemButton from '@mui/material/ListItemButton';
6 | import ListItemText from '@mui/material/ListItemText';
7 | import Collapse from '@mui/material/Collapse';
8 | import ExpandLess from '@mui/icons-material/ExpandLess';
9 | import ExpandMore from '@mui/icons-material/ExpandMore';
10 |
11 | type Props = {
12 | label?: string;
13 | content?: JSX.Element | JSX.Element[] | string;
14 | };
15 |
16 | export default function CollapseList(props: Props) {
17 | const [open, setOpen] = useState(false);
18 |
19 | const handleClick = () => {
20 | setOpen(!open);
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 | {open ? : }
28 |
29 |
35 | {props.content}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/client/components/ConnectDB.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import { set } from 'idb-keyval';
5 | import AES from 'crypto-js/aes';
6 | import { Card, Button, Typography, Divider, Alert } from '@mui/material';
7 |
8 | // import react components
9 | import Input from './Input';
10 |
11 | // import utilities
12 | import useAppStore from '../store/appStore';
13 |
14 | function ConnectDB() {
15 | const username = useAppStore((state) => state.username);
16 | const secret = useAppStore((state) => state.secret);
17 | const userData = useAppStore((state) => state.userData);
18 | const updateUserData = useAppStore((state) => state.updateUserData);
19 |
20 | const [submitAlert, setSubmitAlert] = useState(null);
21 | const [nickname, setNickname] = useState('');
22 | const [uri, setUri] = useState('');
23 | const [host, setHost] = useState('');
24 | const [port, setPort] = useState('5432');
25 | const [database, setDatabase] = useState('');
26 | const [dBUsername, setDBUsername] = useState('');
27 | const [password, setPassword] = useState('');
28 |
29 | // depending on the button clicked, the data submitted either comes from the complete uri string
30 | // or the separated out version
31 | function submitHandler(type: string) {
32 | const copyUserData = { ...userData };
33 | if (type === 'uri') {
34 | copyUserData.dbs.push({
35 | nickname: nickname,
36 | uri: uri,
37 | });
38 | } else if (type === 'separate') {
39 | copyUserData.dbs.push({
40 | nickname: nickname,
41 | uri: `postgres://${dBUsername}:${password}@${host}:${port}/${database}`,
42 | });
43 | }
44 |
45 | updateUserData(copyUserData);
46 |
47 | const ciphertext = AES.encrypt(
48 | JSON.stringify(copyUserData),
49 | secret
50 | ).toString();
51 | set(username, ciphertext).catch((err) => {
52 | console.log('IndexedDB: set failed', err);
53 | });
54 |
55 | setNickname('');
56 | setUri('');
57 | setHost('');
58 | setPort('5432');
59 | setDatabase('');
60 | setDBUsername('');
61 | setPassword('');
62 | setSubmitAlert(
63 |
72 | A new DB has been added
73 |
74 | );
75 | setTimeout(() => {
76 | setSubmitAlert(null);
77 | }, 5000);
78 | }
79 |
80 | return (
81 | <>
82 | {submitAlert}
83 |
93 |
98 | Connect to new DB
99 |
100 |
106 |
107 |
113 | submitHandler('uri')}
117 | >
118 | Submit
119 |
120 |
126 |
132 |
138 |
144 |
151 | submitHandler('separate')}
155 | >
156 | Submit
157 |
158 |
159 | >
160 | );
161 | }
162 |
163 | export default ConnectDB;
164 |
--------------------------------------------------------------------------------
/client/components/DBTab.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import {
5 | Box,
6 | InputLabel,
7 | Select,
8 | SelectChangeEvent,
9 | MenuItem,
10 | } from '@mui/material';
11 |
12 | // import react components
13 | import MetricCard from './MetricCard';
14 | import LineGraphType1 from './LineGraphType1';
15 | import LineGraphType2 from './LineGraphType2';
16 | import PieGraphType1 from './PieGraphType1';
17 |
18 | // import utilities
19 | import { useQueryMetrics } from '../store/rqHooks';
20 |
21 | type Props = {
22 | dbUri: string;
23 | };
24 |
25 | function DBTab(props: Props) {
26 | const [refetchInterval, setRefetchInterval] = useState(15000);
27 |
28 | // react-query custom hook for fetching db metrics from backend
29 | const { isLoading, isError, data } = useQueryMetrics(
30 | ['dbMetrics', props.dbUri],
31 | props.dbUri,
32 | refetchInterval
33 | );
34 |
35 | const handleChangeInterval = (event: SelectChangeEvent) => {
36 | setRefetchInterval(Number(event.target.value));
37 | };
38 |
39 | // coniditonal rendering if error received from fetch
40 | if (isError) {
41 | return (
42 |
43 |
51 | status: request failed!
52 |
53 |
54 | );
55 | } else if (isLoading) {
56 | // coniditonal rendering if fetch has not returned yet
57 | return (
58 |
59 |
67 | status: connecting to db...
68 |
69 |
70 | );
71 | } else {
72 | /* coniditonal rendering if fetch has returned successfully
73 | data sent to child components utilizes optional chaining operators to protect
74 | from fatal errors when nested properties are being accessed in data returned from fetch */
75 | return (
76 |
77 |
85 | Fetch Interval
86 |
92 | 10 Seconds
93 | 15 Seconds
94 | 20 Seconds
95 | 30 Seconds
96 | 1 Minute
97 | 5 Minutes
98 |
99 |
100 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
120 |
121 |
122 |
123 |
124 |
125 |
128 |
129 |
130 |
131 |
132 |
133 |
136 |
137 |
138 |
139 |
140 |
141 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | <>
163 | name: {data.dbStats?.[0].datname}
164 |
165 | id: {data.dbStats?.[0].datid}
166 | >
167 |
168 |
169 | {data.activeSessions}
170 |
171 | {data.conflicts}
172 | {data.deadlocks}
173 |
174 | {data.rolledBackTransactions}
175 |
176 |
177 | {data.transactionsCommitted}
178 |
179 |
180 | {Number(data.cacheHitRatio?.[0].ratio).toFixed(4)}
181 |
182 |
183 | {data.dbStats?.[0].blk_read_time}
184 |
185 |
186 | {data.dbStats?.[0].blk_write_time}
187 |
188 |
189 | {data.dbStats?.[0].blks_hit}
190 |
191 |
192 | {data.dbStats?.[0].blks_read}
193 |
194 |
195 | {data.dbStats?.[0].checksum_failures}
196 |
197 |
198 |
199 | );
200 | }
201 | }
202 |
203 | export default DBTab;
204 |
--------------------------------------------------------------------------------
/client/components/Input.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { Box, TextField } from '@mui/material';
4 |
5 | type Props = {
6 | label: string;
7 | setInput: (eventTargetValue: string) => void;
8 | inputClass?: string;
9 | inputType?: string;
10 | value?: string;
11 | error?: boolean;
12 | errorText?: string;
13 | };
14 |
15 | // reusable input component to package typical functionalities
16 | function Input(props: Props) {
17 | return (
18 |
19 |
20 | {
29 | props.setInput(event.target.value);
30 | }}
31 | />
32 |
33 |
34 | );
35 | }
36 |
37 | export default Input;
38 |
--------------------------------------------------------------------------------
/client/components/LineGraphType1.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import {
4 | Chart,
5 | CategoryScale,
6 | LinearScale,
7 | BarElement,
8 | Title,
9 | Tooltip,
10 | Legend,
11 | LogarithmicScale,
12 | } from 'chart.js';
13 | import { Bar } from 'react-chartjs-2';
14 | import { Box, ListItemText, Typography, Divider } from '@mui/material';
15 |
16 | // import react components
17 | import CollapseList from './CollapseList';
18 |
19 | Chart.register(
20 | CategoryScale,
21 | LinearScale,
22 | BarElement,
23 | Title,
24 | Tooltip,
25 | Legend,
26 | LogarithmicScale
27 | );
28 |
29 | type Props = {
30 | title?: string;
31 | data?: any;
32 | };
33 |
34 | type Line = { labels: string[]; data: number[] };
35 |
36 | function LineGraphType1(props: Props) {
37 | const dataProc: Line = { labels: [], data: [] };
38 | const detailsProc: JSX.Element[] = [];
39 |
40 | if (props.data) {
41 | props.data.all?.rows?.forEach(
42 | (element: { query: string; mean_exec_time: number }) => {
43 | dataProc.labels.push(element.query);
44 | dataProc.data.push(element.mean_exec_time);
45 | }
46 | );
47 |
48 | // used to populate collapsing details list
49 | detailsProc.push(
50 |
54 |
55 | mean
56 |
57 |
58 |
59 | {props.data.mean?.rows?.[0]?.averagequerytime?.toFixed(4)} sec
60 |
61 |
62 | );
63 | detailsProc.push(
64 |
72 |
73 | median
74 |
75 |
76 |
77 | {props.data.median?.rows?.[0]?.median?.toFixed(4)} sec
78 |
79 |
80 | );
81 |
82 | const slowQueries: JSX.Element[] = [];
83 | slowQueries.push(
84 |
85 |
86 | slowest queries
87 |
88 |
89 |
90 | );
91 | props.data.slowestQueries.rows.forEach(
92 | (element: { query: string; mean_exec_time: number }, index: number) => {
93 | slowQueries.push(
94 |
102 |
103 | query {index}: {element.query}
104 |
105 |
106 | time: {element.mean_exec_time?.toFixed(4)} sec
107 |
108 |
109 | );
110 | }
111 | );
112 | detailsProc.push(
113 |
121 | {slowQueries}
122 |
123 | );
124 | }
125 |
126 | // configure ChartJS graph options
127 | const options = {
128 | responsive: true,
129 | plugins: {
130 | legend: {
131 | position: 'top' as const,
132 | },
133 | },
134 | scales: {
135 | y: {
136 | min: 0,
137 | max: 20,
138 | display: true,
139 | title: {
140 | display: true,
141 | text: 'query times [seconds]',
142 | },
143 | },
144 | x: {
145 | display: false,
146 | },
147 | },
148 | };
149 |
150 | const data = {
151 | labels: dataProc.labels,
152 | datasets: [
153 | {
154 | label: 'queries',
155 | data: dataProc.data,
156 | backgroundColor: 'rgba(255, 99, 132, 0.5)',
157 | },
158 | ],
159 | };
160 |
161 | return (
162 | <>
163 | {' '}
164 |
165 | >
166 | );
167 | }
168 |
169 | export default LineGraphType1;
170 |
--------------------------------------------------------------------------------
/client/components/LineGraphType2.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import {
4 | Chart,
5 | CategoryScale,
6 | LinearScale,
7 | BarElement,
8 | Title,
9 | Tooltip,
10 | Legend,
11 | LogarithmicScale,
12 | } from 'chart.js';
13 | import { Bar } from 'react-chartjs-2';
14 | import { Box, ListItemText, Typography } from '@mui/material';
15 |
16 | // import react components
17 | import CollapseList from './CollapseList';
18 |
19 | Chart.register(
20 | CategoryScale,
21 | LinearScale,
22 | BarElement,
23 | Title,
24 | Tooltip,
25 | Legend,
26 | LogarithmicScale
27 | );
28 |
29 | type Props = {
30 | title?: string;
31 | data?: any;
32 | xMax?: number;
33 | };
34 |
35 | type Line = { labels: string[]; data: number[] };
36 |
37 | function LineGraphType2(props: Props) {
38 | const dataProc: Line = { labels: [], data: [] };
39 | const detailsProc: JSX.Element[] = [];
40 |
41 | if (props.data) {
42 | const details: JSX.Element[] = [];
43 | props.data.forEach(
44 | (element: { query: string; mean_exec_time: number }, index: number) => {
45 | dataProc.labels.push(element.query);
46 | dataProc.data.push(element.mean_exec_time);
47 | details.push(
48 |
56 |
57 | query {index}: {element.query}
58 |
59 |
60 | time: {element.mean_exec_time.toFixed(4)} sec
61 |
62 |
63 | );
64 | }
65 | );
66 | detailsProc.push(
67 |
75 | {details}
76 |
77 | );
78 | }
79 |
80 | // configure ChartJS graph options
81 | const options = {
82 | responsive: true,
83 | plugins: {
84 | legend: {
85 | position: 'top' as const,
86 | },
87 | },
88 | scales: {
89 | y: {
90 | min: 0,
91 | max: 5,
92 | display: true,
93 | title: {
94 | display: true,
95 | text: 'query times [seconds]',
96 | },
97 | },
98 | x: {
99 | display: false,
100 | },
101 | },
102 | };
103 |
104 | const data = {
105 | labels: dataProc.labels,
106 | datasets: [
107 | {
108 | label: 'queries',
109 | data: dataProc.data,
110 | backgroundColor: 'rgba(255, 99, 132, 0.5)',
111 | },
112 | ],
113 | };
114 |
115 | return (
116 | <>
117 |
118 |
119 | >
120 | );
121 | }
122 |
123 | export default LineGraphType2;
124 |
--------------------------------------------------------------------------------
/client/components/MetricCard.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import { Card, Typography, Box } from '@mui/material';
5 | import FullscreenIcon from '@mui/icons-material/Fullscreen';
6 | import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
7 |
8 | type Props = {
9 | cardLabel: string;
10 | children?: JSX.Element | string | number;
11 | };
12 |
13 | // mostly styling wrapper for card contents
14 | function MetricCard(props: Props) {
15 | const initialWidth = '400px';
16 | const [width, setWidth] = useState(initialWidth);
17 | const [buttonIcon, setButtonIcon] = useState( );
18 | return (
19 |
29 | {
37 | if (width === initialWidth) {
38 | setWidth(`calc(${window.innerWidth}px - 13.8rem)`);
39 | setButtonIcon( );
40 | } else {
41 | setWidth(initialWidth);
42 | setButtonIcon( );
43 | }
44 | }}
45 | >
46 | {buttonIcon}
47 |
48 | {props.cardLabel}
49 |
54 | {props.children}
55 |
56 |
57 | );
58 | }
59 |
60 | export default MetricCard;
61 |
--------------------------------------------------------------------------------
/client/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useNavigate, useLocation } from 'react-router-dom';
4 | import {
5 | Box,
6 | Drawer,
7 | AppBar,
8 | Toolbar,
9 | List,
10 | Typography,
11 | Divider,
12 | ListItem,
13 | ListItemButton,
14 | ListItemIcon,
15 | ListItemText,
16 | } from '@mui/material';
17 | import LoginIcon from '@mui/icons-material/Login';
18 | import PersonAddIcon from '@mui/icons-material/PersonAdd';
19 | import InfoIcon from '@mui/icons-material/Info';
20 | import TuneIcon from '@mui/icons-material/Tune';
21 | import LogoutIcon from '@mui/icons-material/Logout';
22 | import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
23 | import AccountCircleIcon from '@mui/icons-material/AccountCircle';
24 |
25 | // import utilities
26 | import useAppStore from '../store/appStore';
27 | import postgresql_elephant from '../assets/postgresql_elephant.png';
28 | import yellow_hex from '../assets/yellow_hex.png';
29 |
30 | function Navbar() {
31 | const navigate = useNavigate();
32 | const { pathname } = useLocation();
33 |
34 | const isLoggedIn = useAppStore((state) => state.isLoggedIn);
35 | const username = useAppStore((state) => state.username);
36 | const logOutUser = useAppStore((state) => state.logOutUser);
37 |
38 | const drawerWidth = '11rem';
39 | return (
40 |
41 |
49 |
50 |
59 |
60 | dbHive
61 |
62 | {isLoggedIn && (
63 | <>
64 |
65 |
66 |
76 | {username}
77 |
78 | {
80 | logOutUser();
81 | navigate('/login');
82 | }}
83 | />
84 |
85 | >
86 | )}
87 |
88 |
89 |
101 |
102 |
103 |
104 |
105 |
113 |
114 | for
115 |
116 |
125 |
126 |
127 |
131 | navigate('/login')}>
132 |
133 |
134 |
135 |
136 |
137 |
138 |
142 | navigate('/signup')}>
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | navigate('/')}>
154 |
155 |
156 |
157 |
158 |
159 |
160 | {isLoggedIn && (
161 | <>
162 |
166 | navigate('/setup')}>
167 |
168 |
169 |
170 |
171 |
172 |
173 |
179 | navigate('/dashboard')}>
180 |
181 |
182 |
183 |
184 |
185 |
186 | >
187 | )}
188 |
189 |
190 |
191 |
192 | );
193 | }
194 |
195 | export default Navbar;
196 |
--------------------------------------------------------------------------------
/client/components/PieGraphType1.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { Chart, ArcElement, Tooltip, Legend } from 'chart.js';
4 | import { Pie } from 'react-chartjs-2';
5 |
6 | Chart.register(ArcElement, Tooltip, Legend);
7 |
8 | type Props = {
9 | data?: any;
10 | };
11 |
12 | type Pie = {
13 | 'time < .1s'?: number;
14 | '.1s > time < .5s'?: number;
15 | '.5s > time < 1s'?: number;
16 | '1s < time'?: number;
17 | };
18 |
19 | function PieGraphType1(props: Props) {
20 | // sort data into intervals
21 | const dataProc: Pie = {
22 | 'time < .1s': 0,
23 | '.1s > time < .5s': 0,
24 | '.5s > time < 1s': 0,
25 | '1s < time': 0,
26 | };
27 |
28 | if (props.data) {
29 | props.data.forEach((element: { query: string; mean_exec_time: number }) => {
30 | if (element.mean_exec_time < 0.1) {
31 | dataProc['time < .1s']++;
32 | } else if (element.mean_exec_time > 0.1 && element.mean_exec_time < 0.5) {
33 | dataProc['.1s > time < .5s']++;
34 | } else if (element.mean_exec_time > 0.5 && element.mean_exec_time < 1) {
35 | dataProc['.5s > time < 1s']++;
36 | } else if (element.mean_exec_time > 1) {
37 | dataProc['1s < time']++;
38 | }
39 | });
40 | }
41 |
42 | const data = {
43 | labels: Object.keys(dataProc),
44 | datasets: [
45 | {
46 | label: 'Dataset 1',
47 | data: Object.values(dataProc),
48 | backgroundColor: [
49 | 'rgba(255, 99, 132, 0.2)',
50 | 'rgba(54, 162, 235, 0.2)',
51 | 'rgba(255, 206, 86, 0.2)',
52 | 'rgba(75, 192, 192, 0.2)',
53 | 'rgba(153, 102, 255, 0.2)',
54 | 'rgba(255, 159, 64, 0.2)',
55 | ],
56 | borderColor: [
57 | 'rgba(255, 99, 132, 1)',
58 | 'rgba(54, 162, 235, 1)',
59 | 'rgba(255, 206, 86, 1)',
60 | 'rgba(75, 192, 192, 1)',
61 | 'rgba(153, 102, 255, 1)',
62 | 'rgba(255, 159, 64, 1)',
63 | ],
64 | borderWidth: 1,
65 | },
66 | ],
67 | };
68 |
69 | return ;
70 | }
71 |
72 | export default PieGraphType1;
73 |
--------------------------------------------------------------------------------
/client/import-images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png';
2 | declare module '*.jpg';
3 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | dbHive
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 | import './styles.css';
5 |
6 | const root = createRoot(document.getElementById('root'));
7 | root.render( );
8 |
--------------------------------------------------------------------------------
/client/pages/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState, useEffect } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | import { Box, Card, Tabs, Tab, Typography, Button } from '@mui/material';
6 |
7 | // import react components
8 | import Navbar from '../components/Navbar';
9 | import DBTab from '../components/DBTab';
10 |
11 | // import utilities
12 | import { toggleDashboardAuth } from '../clientMode';
13 | import useAppStore from '../store/appStore';
14 |
15 | function Dashboard() {
16 | const navigate = useNavigate();
17 |
18 | const isLoggedIn = useAppStore((state) => state.isLoggedIn);
19 | const userData = useAppStore((state) => state.userData);
20 |
21 | const [activeTab, setActiveTab] = useState(0);
22 |
23 | // check user authorization
24 | useEffect(() => {
25 | if (!isLoggedIn && toggleDashboardAuth) navigate('/login');
26 | }, []);
27 |
28 | // conditional rendering for when user has not added any databases
29 | if (userData.dbs[0] === undefined) {
30 | return (
31 |
32 |
33 |
42 |
43 |
44 |
53 |
54 | Please connect a database
55 |
56 | navigate('/setup')}
60 | >
61 | Setup
62 |
63 |
64 |
65 | );
66 | }
67 | // default rendering for when user has already added databases
68 | else {
69 | type TabList = JSX.Element[];
70 | const tabList: TabList = [];
71 | const tabPanel: TabList = [];
72 | userData.dbs.forEach((db, index) => {
73 | tabPanel.push(
74 |
75 |
76 |
77 | );
78 | tabList.push(
79 |
83 |
84 |
91 |
92 |
93 |
94 |
95 |
96 | {db.nickname}
97 |
98 | >
99 | }
100 | onClick={() => {
101 | setActiveTab(index);
102 | }}
103 | />
104 | );
105 | });
106 |
107 | return (
108 |
109 |
110 |
119 |
120 | {tabList}
121 |
122 |
123 | {tabPanel}
124 |
125 | );
126 | }
127 | }
128 |
129 | export default Dashboard;
130 |
--------------------------------------------------------------------------------
/client/pages/Docs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Navbar from '../components/Navbar';
3 | import {
4 | Button,
5 | Box,
6 | Card,
7 | Typography,
8 | Divider,
9 | Stepper,
10 | Step,
11 | StepLabel,
12 | StepContent,
13 | } from '@mui/material';
14 | import dbhive_logo_transparent from '../assets/dbhive_logo_transparent.png';
15 |
16 | function Docs() {
17 | return (
18 | <>
19 |
20 |
25 |
37 |
45 |
55 | Welcome to dbHive
56 |
69 |
70 |
71 | dbHive offers an interactive dashboard to visualize the performance
72 | of one or more PostgreSQL databases. By providing easily accessible
73 | information about the health and activity of a database, dbHive
74 | enables developers to make informed decisions that optimize the way
75 | they store their data.
76 |
77 |
78 |
79 | Learn More
80 |
81 |
82 |
83 |
93 |
102 | Get Started
103 |
104 |
105 |
106 |
107 |
108 | Sign Up
109 |
110 |
113 |
114 | Create an account that can be utilized by one or more users.
115 |
116 |
117 |
118 |
119 |
120 | Login
121 |
122 |
125 |
126 | Login with your username and password information.
127 |
128 |
129 |
130 |
131 |
132 | Connect Databases
133 |
134 |
137 |
138 | Connect one or more PostgreSQL databases using either the URI
139 | string or individual credentials.
140 |
141 |
142 |
143 |
144 |
145 |
146 | Use the Dashboard to monitor your databases!
147 |
148 |
149 |
152 |
153 | Access query execution times, most frequent queries,
154 | conflicts, deadlocks, rolled back transactions, cache hit
155 | ratio, block hits, and more.
156 |
157 |
158 |
159 |
160 |
161 |
171 |
180 | Troubleshooting
181 |
182 |
183 |
184 | If certain database metrics are not showing up in the dashboard,
185 | look into the database user's permissions. User permissions and
186 | admin privileges can vary depending on the database hosting service
187 | used.
188 |
189 |
190 |
191 | >
192 | );
193 | }
194 |
195 | export default Docs;
196 |
--------------------------------------------------------------------------------
/client/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | import { get } from 'idb-keyval';
6 | import CryptoJS from 'crypto-js';
7 | import AES from 'crypto-js/aes';
8 | import { Card, Button, Typography, Box } from '@mui/material';
9 |
10 | // import react components
11 | import Navbar from '../components/Navbar';
12 | import Input from '../components/Input';
13 |
14 | // import utilities
15 | import useAppStore from '../store/appStore';
16 |
17 | function Login() {
18 | const navigate = useNavigate();
19 |
20 | const logInUser = useAppStore((state) => state.logInUser);
21 |
22 | const [usernameInput, setUsernameInput] = useState('');
23 | const [secretInput, setSecretInput] = useState('');
24 | const [loginErrorText, setLoginErrorText] = useState(null);
25 |
26 | function submitHandler() {
27 | /* authenticate by checking for a username key within IndexedDB
28 | if there is a user, use the password as the AES encryption secret,
29 | and look for a verifiable property on the value object
30 | */
31 | get(usernameInput)
32 | .then((data) => {
33 | const bytes = AES.decrypt(data, secretInput);
34 | const decryptResponse = bytes.toString(CryptoJS.enc.Utf8);
35 | const originalText = JSON.parse(decryptResponse);
36 | if (originalText.decryption === 'isValid') {
37 | // populate global state store with decrypted IDB value, which holds user data
38 | logInUser(usernameInput, secretInput, originalText);
39 | setUsernameInput('');
40 | setSecretInput('');
41 | setLoginErrorText(null);
42 | navigate('/dashboard');
43 | } else {
44 | setLoginErrorText('incorrect username or password');
45 | }
46 | })
47 | .catch(() => {
48 | setLoginErrorText('incorrect username or password');
49 | });
50 | }
51 |
52 | return (
53 |
54 |
55 |
60 |
69 |
75 | Login
76 |
77 |
84 |
93 |
98 | Submit
99 |
100 | navigate('/signup')}
104 | >
105 | Sign Up
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
113 | export default Login;
114 |
--------------------------------------------------------------------------------
/client/pages/Setup.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useEffect } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | import { set } from 'idb-keyval';
6 | import AES from 'crypto-js/aes';
7 | import { Card, Typography, ListItemText, List, Box } from '@mui/material';
8 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
9 |
10 | // import react components
11 | import Navbar from '../components/Navbar';
12 | import ConnectDB from '../components/ConnectDB';
13 |
14 | // import utilities
15 | import useAppStore from '../store/appStore';
16 | import { UserData } from '../clientTypes';
17 |
18 | function Setup() {
19 | const navigate = useNavigate();
20 |
21 | const username = useAppStore((state) => state.username);
22 | const secret = useAppStore((state) => state.secret);
23 | const isLoggedIn = useAppStore((state) => state.isLoggedIn);
24 | const userData = useAppStore((state) => state.userData);
25 | const updateUserData = useAppStore((state) => state.updateUserData);
26 |
27 | // check user authorization
28 | useEffect(() => {
29 | if (!isLoggedIn) navigate('/login');
30 | }, []);
31 |
32 | function deleteHandler(dbName: string) {
33 | const copyUserData = { ...userData };
34 | copyUserData.dbs = copyUserData.dbs.filter((db) => db.nickname !== dbName);
35 | updateUserData(copyUserData);
36 | storeDelete(copyUserData);
37 | }
38 |
39 | function storeDelete(userData: UserData) {
40 | const ciphertext = AES.encrypt(JSON.stringify(userData), secret).toString();
41 | set(username, ciphertext).catch((err) => {
42 | console.log('IndexedDB: set failed', err);
43 | });
44 | }
45 |
46 | // render a list of databases associated with the user
47 | const connectedDBs: JSX.Element[] = [];
48 | userData.dbs.reverse().forEach((db) => {
49 | connectedDBs.push(
50 |
60 | {
68 | deleteHandler(db.nickname);
69 | }}
70 | >
71 |
72 |
73 |
80 |
81 |
82 |
83 |
84 |
85 | {db.nickname}
86 |
87 | );
88 | });
89 |
90 | return (
91 |
96 |
97 |
98 |
107 |
108 | Connected Databases ({userData.dbs.length})
109 |
110 | {connectedDBs}
111 |
112 |
113 | );
114 | }
115 |
116 | export default Setup;
117 |
--------------------------------------------------------------------------------
/client/pages/Signup.tsx:
--------------------------------------------------------------------------------
1 | // import dependencies
2 | import * as React from 'react';
3 | import { useState } from 'react';
4 | import { useNavigate } from 'react-router-dom';
5 | import { set, get } from 'idb-keyval';
6 | import AES from 'crypto-js/aes';
7 | import { Card, Button, Typography, Box } from '@mui/material';
8 |
9 | // import react components
10 | import Navbar from '../components/Navbar';
11 | import Input from '../components/Input';
12 |
13 | // import utilities
14 | import { UserData } from '../clientTypes';
15 |
16 | function Signup() {
17 | const navigate = useNavigate();
18 |
19 | const [usernameInput, setUsernameInput] = useState('');
20 | const [secretInput, setSecretInput] = useState('');
21 | const [signupErrorText, setSignupErrorText] = useState(null);
22 |
23 | function submitHandler() {
24 | /* check to see if the username is available by searching IndexedDB
25 | if available, add username as key to IndexedDB and populate value with an encrypted
26 | JSON object using the password as the AES secret
27 | */
28 | const initialUserData: UserData = { decryption: 'isValid', dbs: [] };
29 | const ciphertext = AES.encrypt(
30 | JSON.stringify(initialUserData),
31 | secretInput
32 | ).toString();
33 |
34 | get(usernameInput)
35 | .then((data) => {
36 | if (data === undefined) {
37 | set(usernameInput, ciphertext)
38 | .then(() => {
39 | navigate('/login');
40 | })
41 | .catch((err) => {
42 | console.log('IndexedDB: set failed', err);
43 | });
44 | } else {
45 | setSignupErrorText('incorrect username or password');
46 | }
47 | })
48 | .catch((err) => {
49 | console.log('IndexedDB: get failed', err);
50 | });
51 |
52 | setUsernameInput('');
53 | setSecretInput('');
54 | setSignupErrorText(null);
55 | }
56 |
57 | return (
58 |
59 |
60 |
65 |
74 |
80 | Sign Up
81 |
82 |
89 |
98 |
103 | Submit
104 |
105 |
106 |
107 |
108 | );
109 | }
110 |
111 | export default Signup;
112 |
--------------------------------------------------------------------------------
/client/store/appStore.ts:
--------------------------------------------------------------------------------
1 | // zustand store for global state
2 | import create from 'zustand';
3 | import { UserData } from '../clientTypes';
4 |
5 | interface AppState {
6 | username: string;
7 | secret: string;
8 | isLoggedIn: boolean;
9 | logInUser: (username: string, secret: string, userData: UserData) => void;
10 | logOutUser: () => void;
11 | userData: UserData;
12 | updateUserData: (userData: UserData) => void;
13 | }
14 |
15 | const useAppStore = create()((set) => ({
16 | username: '',
17 | secret: '',
18 | isLoggedIn: false,
19 | logInUser: (username, secret, userData) =>
20 | set(() => ({
21 | username: username,
22 | secret: secret,
23 | isLoggedIn: true,
24 | userData: userData,
25 | })),
26 | logOutUser: () =>
27 | set(() => ({
28 | username: '',
29 | secret: '',
30 | isLoggedIn: false,
31 | userData: {
32 | decryption: 'isValid',
33 | dbs: [],
34 | },
35 | })),
36 | userData: {
37 | decryption: 'isValid',
38 | dbs: [],
39 | },
40 | updateUserData: (userData) =>
41 | set(() => ({
42 | userData: userData,
43 | })),
44 | }));
45 |
46 | export default useAppStore;
47 |
--------------------------------------------------------------------------------
/client/store/rqHooks.ts:
--------------------------------------------------------------------------------
1 | // react-query custom hooks
2 | import { useQuery } from 'react-query';
3 |
4 | // custom hook to fetch metrics from the backend and handle state
5 | export function useQueryMetrics(key: string[], uri: string, interval: number) {
6 | const { isLoading, isError, data } = useQuery(
7 | key,
8 | async () => {
9 | const res = await fetch('/api/queryMetrics', {
10 | method: 'POST',
11 | headers: {
12 | 'Content-Type': 'Application/JSON',
13 | },
14 | body: JSON.stringify({ uri: uri }),
15 | });
16 | if (!res.ok) {
17 | throw new Error('Network response was not ok');
18 | }
19 | return res.json();
20 | },
21 | { refetchInterval: interval, refetchIntervalInBackground: true }
22 | );
23 | return { isLoading, isError, data };
24 | }
25 |
--------------------------------------------------------------------------------
/client/styles.css:
--------------------------------------------------------------------------------
1 | /* ANIMATED BEE LOGO */
2 | .pulse-animation {
3 | animation: createBox 1s infinite;
4 | }
5 |
6 | @keyframes createBox {
7 | from {
8 | transform: scale(1);
9 | }
10 | to {
11 | transform: scale(1.25);
12 | }
13 | }
14 |
15 | /* SCROLLBAR */
16 | /* width */
17 | ::-webkit-scrollbar {
18 | width: 10px;
19 | height: 10px;
20 | }
21 |
22 | /* Track */
23 | ::-webkit-scrollbar-track {
24 | background: rgb(255, 255, 255, 0.1);
25 | }
26 |
27 | /* Handle */
28 | ::-webkit-scrollbar-thumb {
29 | background: rgb(255, 255, 255, 0.5);
30 | }
31 |
32 | /* Handle on hover */
33 | ::-webkit-scrollbar-thumb:hover {
34 | background: rgb(255, 255, 255, 0.8);
35 | }
36 |
--------------------------------------------------------------------------------
/db-sample-data/adventureworks.readme:
--------------------------------------------------------------------------------
1 | Instructions:
2 |
3 | - Within database, create public schema => CREATE SCHEMA public;
4 | - Navigate to db-sample-data dir
5 | - Unzip csv-data and place all csv files inside same dir as .rb and .sql scripts
6 | - Inside terminal run => psql -h [HOST ADDRESS] -p [PORT] -d [DB NAME] -U [USERNAME] < install.sql
--------------------------------------------------------------------------------
/db-sample-data/csv-data.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/db-sample-data/csv-data.zip
--------------------------------------------------------------------------------
/db-sample-data/update_csvs.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # AdventureWorks for Postgres
4 | # by Lorin Thwaits
5 |
6 | # How to use this file:
7 |
8 | # Download "Adventure Works 2014 OLTP Script" from:
9 | # https://msftdbprodsamples.codeplex.com/downloads/get/880662
10 |
11 | # Extract the .zip and copy all of the CSV files into the same folder containing
12 | # this update_csvs.rb file and the install.sql file.
13 |
14 | # Modify the CSVs to work with Postgres by running:
15 | # ruby update_csvs.rb
16 |
17 | # Create the database and tables, import the data, and set up the views and keys with:
18 | # psql -c "CREATE DATABASE \"Adventureworks\";"
19 | # psql -d Adventureworks < install.sql
20 |
21 | # (you may need to also add: -U postgres to the above two commands)
22 |
23 | # All 68 tables are properly set up.
24 | # All 20 views are established.
25 | # 68 additional convenience views are added which:
26 | # * Provide a shorthand to refer to tables.
27 | # * Add an "id" column to a primary key or primary-ish key if it makes sense.
28 |
29 | # For example, with the convenience views you can simply do:
30 | # SELECT pe.p.firstname, hr.e.jobtitle
31 | # FROM pe.p
32 | # INNER JOIN hr.e ON pe.p.id = hr.e.id;
33 | # Instead of:
34 | # SELECT p.firstname, e.jobtitle
35 | # FROM person.person AS p
36 | # INNER JOIN humanresources.employee AS e ON p.businessentityid = e.businessentityid;
37 |
38 | # Schemas for these views:
39 | # pe = person
40 | # hr = humanresources
41 | # pr = production
42 | # pu = purchasing
43 | # sa = sales
44 | # Easily get a list of all of these in psql with: \dv (pe|hr|pr|pu|sa).*
45 |
46 | # Enjoy!
47 |
48 |
49 | Dir.glob('./*.csv') do |csv_file|
50 | f = File.open(csv_file, "rb:UTF-16LE:utf-8")
51 | output = ""
52 | text = ""
53 | is_needed=false
54 | is_first = true
55 | is_pipes = false
56 | begin
57 | f.each do |line|
58 | if is_first
59 | if line.include?("+|")
60 | is_pipes = true
61 | end
62 | if line[0] == "\uFEFF"
63 | line = line[1..-1]
64 | is_needed = true
65 | end
66 | end
67 | is_first = false
68 | break if !is_needed
69 | if is_pipes
70 | if line.strip.end_with?("&|")
71 | text << line.gsub(/\"/, "\"\"").strip[0..-3]
72 | output << text.split("+|").map { |part|
73 | (part[1] == "<" && part[-1] == ">") ? '"' + part[1..-1] + '"' :
74 | (part.include?("\t") ? '"' + part + '"' : part)
75 | }.join("\t")
76 | output << "\n"
77 | text = ""
78 | else
79 | text << line.gsub(/\"/, "\"\"").gsub("\r\n", "\\n")
80 | end
81 | else
82 | # The last gsub makes everything compatible with Windows -- change \r\n into just \n
83 | output << line.gsub(/\"/, "\"\"").gsub(/\&\|\n/, "\n").gsub(/\&\|\r\n/, "\n").gsub(/\r\n/, "\n")
84 | end
85 | end
86 | if is_needed
87 | puts "Processing #{csv_file}"
88 | f.close
89 | w = File.open(csv_file + ".xyz", "w")
90 | w.write(output)
91 | w.close
92 | File.delete(csv_file)
93 | File.rename(csv_file + ".xyz", csv_file)
94 | end
95 |
96 | # Here's a list of files that get snagged here:
97 | # Address.csv
98 | # AWBuildVersion.csv
99 | # CreditCard.csv
100 | # Culture.csv
101 | # Currency.csv
102 | # Department.csv
103 | # EmployeeDepartmentHistory.csv
104 | # EmployeePayHistory.csv
105 | # Product.csv
106 | # ProductCostHistory.csv
107 | # ProductModelIllustration.csv
108 | # ProductReview.csv
109 | # SalesOrderDetail.csv
110 | # SalesTerritory.csv
111 | # Shift.csv
112 | # ShipMethod.csv
113 | # ShoppingCartItem.csv
114 | # SpecialOffer.csv
115 | # Vendor.csv
116 | # WorkOrder.csv
117 | rescue Encoding::InvalidByteSequenceError
118 | f.close
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/dist/client/App.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function App(): JSX.Element;
3 | export default App;
4 |
--------------------------------------------------------------------------------
/dist/client/clientMode.d.ts:
--------------------------------------------------------------------------------
1 | import { UserData } from './clientTypes';
2 | declare const toggleDashboardAuth = true;
3 | declare const seedDBs: UserData['dbs'];
4 | export { toggleDashboardAuth, seedDBs };
5 |
--------------------------------------------------------------------------------
/dist/client/clientTypes.d.ts:
--------------------------------------------------------------------------------
1 | export type UserData = {
2 | decryption: string;
3 | dbs: {
4 | nickname: string;
5 | uri: string;
6 | }[];
7 | };
8 | export type DbData = {
9 | nickname: string;
10 | uri: string;
11 | };
12 |
--------------------------------------------------------------------------------
/dist/client/components/CollapseList.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | label?: string;
4 | content?: JSX.Element | JSX.Element[] | string;
5 | };
6 | export default function CollapseList(props: Props): JSX.Element;
7 | export {};
8 |
--------------------------------------------------------------------------------
/dist/client/components/ConnectDB.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function ConnectDB(): JSX.Element;
3 | export default ConnectDB;
4 |
--------------------------------------------------------------------------------
/dist/client/components/DBTab.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | dbUri: string;
4 | };
5 | declare function DBTab(props: Props): JSX.Element;
6 | export default DBTab;
7 |
--------------------------------------------------------------------------------
/dist/client/components/DropdownMenu.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/dbhive/64f440be09f0b0d0d4b8323f49fbd82294ec0aff/dist/client/components/DropdownMenu.d.ts
--------------------------------------------------------------------------------
/dist/client/components/ErrorBoundary.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Component, ErrorInfo, ReactNode } from 'react';
3 | interface Props {
4 | children?: ReactNode;
5 | }
6 | interface State {
7 | hasError: boolean;
8 | }
9 | declare class ErrorBoundary extends Component {
10 | state: State;
11 | static getDerivedStateFromError(_: Error): State;
12 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
13 | render(): string | number | boolean | React.ReactFragment | JSX.Element;
14 | }
15 | export default ErrorBoundary;
16 |
--------------------------------------------------------------------------------
/dist/client/components/Graph1.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | labels?: string[];
4 | data?: number[];
5 | };
6 | declare function Graph1(props: Props): JSX.Element;
7 | export default Graph1;
8 |
--------------------------------------------------------------------------------
/dist/client/components/Graph2.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | labels?: string[];
4 | data?: number[];
5 | };
6 | declare function Graph2(props: Props): JSX.Element;
7 | export default Graph2;
8 |
--------------------------------------------------------------------------------
/dist/client/components/Input.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | label: string;
4 | setInput: (eventTargetValue: string) => void;
5 | inputClass?: string;
6 | inputType?: string;
7 | value?: string;
8 | error?: boolean;
9 | errorText?: string;
10 | };
11 | declare function Input(props: Props): JSX.Element;
12 | export default Input;
13 |
--------------------------------------------------------------------------------
/dist/client/components/LineGraphType1.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | title?: string;
4 | data?: any;
5 | };
6 | declare function LineGraphType1(props: Props): JSX.Element;
7 | export default LineGraphType1;
8 |
--------------------------------------------------------------------------------
/dist/client/components/LineGraphType2.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | title?: string;
4 | data?: any;
5 | xMax?: number;
6 | };
7 | declare function LineGraphType2(props: Props): JSX.Element;
8 | export default LineGraphType2;
9 |
--------------------------------------------------------------------------------
/dist/client/components/MetricCard.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | cardLabel: string;
4 | children?: JSX.Element | string | number;
5 | };
6 | declare function MetricCard(props: Props): JSX.Element;
7 | export default MetricCard;
8 |
--------------------------------------------------------------------------------
/dist/client/components/Navbar.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Navbar(): JSX.Element;
3 | export default Navbar;
4 |
--------------------------------------------------------------------------------
/dist/client/components/PieGraphType1.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | type Props = {
3 | data?: any;
4 | };
5 | declare function PieGraphType1(props: Props): JSX.Element;
6 | export default PieGraphType1;
7 |
--------------------------------------------------------------------------------
/dist/client/index.d.ts:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 |
--------------------------------------------------------------------------------
/dist/client/pages/Dashboard.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Dashboard(): JSX.Element;
3 | export default Dashboard;
4 |
--------------------------------------------------------------------------------
/dist/client/pages/Docs.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Docs(): JSX.Element;
3 | export default Docs;
4 |
--------------------------------------------------------------------------------
/dist/client/pages/Home.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Home(): JSX.Element;
3 | export default Home;
4 |
--------------------------------------------------------------------------------
/dist/client/pages/Login.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Login(): JSX.Element;
3 | export default Login;
4 |
--------------------------------------------------------------------------------
/dist/client/pages/Setup.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Setup(): JSX.Element;
3 | export default Setup;
4 |
--------------------------------------------------------------------------------
/dist/client/pages/Signup.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare function Signup(): JSX.Element;
3 | export default Signup;
4 |
--------------------------------------------------------------------------------
/dist/client/store/appStore.d.ts:
--------------------------------------------------------------------------------
1 | import { UserData } from '../clientTypes';
2 | interface AppState {
3 | username: string;
4 | secret: string;
5 | isLoggedIn: boolean;
6 | logInUser: (username: string, secret: string, userData: UserData) => void;
7 | logOutUser: () => void;
8 | userData: UserData;
9 | updateUserData: (userData: UserData) => void;
10 | }
11 | declare const useAppStore: import("zustand").UseBoundStore>;
12 | export default useAppStore;
13 |
--------------------------------------------------------------------------------
/dist/client/store/rqHooks.d.ts:
--------------------------------------------------------------------------------
1 | export declare function useQueryMetrics(key: string[], uri: string, interval: number): {
2 | isLoading: boolean;
3 | isError: boolean;
4 | data: any;
5 | };
6 |
--------------------------------------------------------------------------------
/docker-compose-dev-hot.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | dev:
4 | image: dbhive/main-dev
5 | container_name: main-dev
6 | ports:
7 | - 8080:8080
8 | volumes:
9 | - .:/usr/src/app
10 | - node_modules:/usr/src/app/node_modules
11 | command: npm run dev
12 | volumes:
13 | node_modules:
14 |
--------------------------------------------------------------------------------
/docker-compose-test.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | test:
4 | image: dbhive/main-prod
5 | environment:
6 | - TEST_DB=${TEST_DB}
7 | container_name: main-test
8 | ports:
9 | - 3000:3000
10 | volumes:
11 | - .:/usr/src/app
12 | - node_modules:/usr/src/app/node_modules
13 | command: npm run test
14 | volumes:
15 | node_modules:
16 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | automock: false,
3 | //ts jest or jest
4 | preset: 'ts-jest',
5 | testEnvironment: 'jest-environment-jsdom',
6 | transform: {
7 | '^.+\\.(ts|tsx)?$': 'ts-jest',
8 | '^.+\\.(js|jsx)$': 'babel-jest',
9 | },
10 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
11 | moduleNameMapper: {
12 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
13 | '/__tests__/__mocks__/fileMock.ts',
14 | //if this doesn't work, go to filemock
15 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
16 | },
17 | testPathIgnorePatterns: [
18 | './__tests__/__mocks__',
19 | './__tests__/jest-setup.js',
20 | './__tests__/jest-teardown.js',
21 | ],
22 | globalSetup: './__tests__/jest-setup.js',
23 | globalTeardown: './__tests__/jest-teardown.js',
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dbhive",
3 | "version": "1.0.0",
4 | "description": "postgres performance analysis tool",
5 | "main": "index.tsx",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=production ts-node --transpile-only server/server.ts",
8 | "build": "cross-env NODE_ENV=production webpack",
9 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open --hot\" \"cross-env NODE_ENV=development nodemon server/server.ts\"",
10 | "test": "jest --config jest.config.js --detectOpenHandles"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/oslabs-beta/dbhive.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/oslabs-beta/dbhive/issues"
21 | },
22 | "homepage": "https://github.com/oslabs-beta/dbhive#readme",
23 | "dependencies": {
24 | "@emotion/react": "^11.10.5",
25 | "@emotion/styled": "^11.10.5",
26 | "@mui/icons-material": "^5.10.16",
27 | "@mui/material": "^5.10.16",
28 | "@types/chart.js": "^2.9.37",
29 | "@types/crypto-js": "^4.1.1",
30 | "@types/eslint": "^8.4.10",
31 | "@types/express": "^4.17.14",
32 | "@types/jest": "^29.2.3",
33 | "@types/node": "^18.11.9",
34 | "@types/pg": "^8.6.5",
35 | "@types/react": "^18.0.25",
36 | "@types/react-dom": "^18.0.9",
37 | "@types/react-is": "^17.0.3",
38 | "@types/react-router": "^5.1.19",
39 | "@types/regenerator-runtime": "^0.13.1",
40 | "babel-jest": "^29.3.1",
41 | "bcryptjs": "^2.4.3",
42 | "chart.js": "3.9.1",
43 | "cross-env": "^7.0.3",
44 | "crypto-js": "^4.1.1",
45 | "express": "^4.18.2",
46 | "idb-keyval": "^6.2.0",
47 | "jest-environment-jsdom": "^29.3.1",
48 | "material-ui-popup-state": "^4.1.0",
49 | "path": "^0.12.7",
50 | "pg": "^8.8.0",
51 | "react": "^18.2.0",
52 | "react-chartjs-2": "4.3.1",
53 | "react-dom": "^18.2.0",
54 | "react-query": "^3.39.2",
55 | "react-router": "^6.4.3",
56 | "react-router-dom": "^6.4.3",
57 | "regenerator-runtime": "^0.13.11",
58 | "ts-jest": "^29.0.3",
59 | "ts-node": "^10.9.1",
60 | "typescript": "^4.9.4",
61 | "zustand": "^4.1.5"
62 | },
63 | "devDependencies": {
64 | "@babel/core": "^7.19.6",
65 | "@babel/preset-env": "^7.20.2",
66 | "@babel/preset-react": "^7.18.6",
67 | "@testing-library/jest-dom": "^5.16.5",
68 | "@testing-library/react": "^13.4.0",
69 | "@testing-library/user-event": "^14.4.3",
70 | "@types/express": "^4.17.14",
71 | "@types/node": "^18.11.9",
72 | "@typescript-eslint/eslint-plugin": "^5.43.0",
73 | "@typescript-eslint/parser": "^5.43.0",
74 | "babel-loader": "^9.0.0",
75 | "concurrently": "^7.6.0",
76 | "css-loader": "^6.7.2",
77 | "eslint": "^8.27.0",
78 | "eslint-import-resolver-node": "^0.3.6",
79 | "eslint-import-resolver-typescript": "^3.5.2",
80 | "eslint-plugin-import": "^2.26.0",
81 | "file-loader": "^6.2.0",
82 | "html-webpack-plugin": "^5.5.0",
83 | "install": "^0.13.0",
84 | "isomorphic-fetch": "^3.0.0",
85 | "jest": "^29.3.1",
86 | "node-loader": "^2.0.0",
87 | "nodemon": "^2.0.20",
88 | "npm": "^9.1.2",
89 | "prettier": "2.8.0",
90 | "resolve-url-loader": "^5.0.0",
91 | "sass": "^1.55.0",
92 | "sass-loader": "^13.1.0",
93 | "style-loader": "^3.3.1",
94 | "supertest": "^6.3.1",
95 | "ts-loader": "^9.4.1",
96 | "url-loader": "^4.1.1",
97 | "webpack": "^5.74.0",
98 | "webpack-cli": "^4.10.0",
99 | "webpack-dev-server": "^4.11.1"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/server/controllers/connectController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from 'express';
2 | import { Pool } from 'pg';
3 |
4 | type ConnectController = {
5 | connectDB: RequestHandler;
6 | createExtension: RequestHandler;
7 | };
8 |
9 | // on request, connect to user's database and return query pool on res.locals
10 | const connectController: ConnectController = {
11 | connectDB: (req, res, next) => {
12 | const uri_string = req.body.uri;
13 | const pool = new Pool({
14 | connectionString: uri_string,
15 | });
16 | const db = {
17 | query: (text: string, params?: Array) => {
18 | return pool.query(text, params);
19 | },
20 | };
21 | res.locals.dbConnection = db;
22 | res.locals.result = {};
23 | return next();
24 | },
25 |
26 | // initializes pg_stat_statements if not already initialized
27 | // first controller to stop response cycle and return an error if connection fails
28 | createExtension: async (req, res, next) => {
29 | const db = res.locals.dbConnection;
30 | const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements';
31 | try {
32 | await db.query(queryString);
33 | res.locals.result.validURI = true;
34 | return next();
35 | } catch (error) {
36 | return next({
37 | log: `ERROR caught in connectController.createExtension: ${error}`,
38 | status: 400,
39 | message:
40 | 'ERROR: error has occured in connectController.createExtension',
41 | });
42 | }
43 | },
44 | };
45 |
46 | export default connectController;
47 |
--------------------------------------------------------------------------------
/server/controllers/databaseController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler } from 'express';
2 |
3 | type DatabaseController = {
4 | queryTimes: RequestHandler;
5 | numOfRows: RequestHandler;
6 | topCalls: RequestHandler;
7 | dbStats: RequestHandler;
8 | cacheHitRatio: RequestHandler;
9 | statActivity: RequestHandler;
10 | };
11 | type queryData = {
12 | all: any[];
13 | mean: number;
14 | median: number;
15 | slowestQueries: any[];
16 | } | null;
17 |
18 | const databaseController: DatabaseController = {
19 | // this controller returns mean and median times for different types of queries
20 | queryTimes: async (req, res, next) => {
21 | const db = res.locals.dbConnection;
22 | // WIP: this number will come from the front end and currently defaults to 10
23 | const userNumberOfQueries: number = req.body.slowQueries;
24 | const slowQueryNumber = userNumberOfQueries || 10;
25 |
26 | try {
27 | const allQueries: queryData = {
28 | all: await db.query(
29 | 'SELECT * FROM pg_stat_statements ORDER BY mean_exec_time;'
30 | ),
31 | median: await db.query(
32 | 'SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY mean_exec_time) AS median FROM pg_stat_statements;'
33 | ),
34 | mean: await db.query(
35 | 'SELECT avg(mean_exec_time) AS averageQueryTime FROM pg_stat_statements;'
36 | ),
37 | slowestQueries: await db.query(
38 | 'SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT $1;',
39 | [slowQueryNumber]
40 | ),
41 | };
42 | res.locals.result.allTimes = allQueries;
43 |
44 | const selectQueries: queryData = {
45 | all: await db.query(
46 | "SELECT * FROM pg_stat_statements WHERE query LIKE '%SELECT%' ORDER BY mean_exec_time;"
47 | ),
48 | median: await db.query(
49 | "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY mean_exec_time) AS median FROM pg_stat_statements WHERE query LIKE '%SELECT%';"
50 | ),
51 | mean: await db.query(
52 | "SELECT avg(mean_exec_time) AS averageQueryTime FROM pg_stat_statements WHERE query LIKE '%SELECT%';"
53 | ),
54 | slowestQueries: await db.query(
55 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%SELECT%' ORDER BY mean_exec_time DESC LIMIT $1;",
56 | [slowQueryNumber]
57 | ),
58 | };
59 | res.locals.result.selectTimes = selectQueries;
60 |
61 | const insertQueries: queryData = {
62 | all: await db.query(
63 | "SELECT * FROM pg_stat_statements WHERE query LIKE '%INSERT%' ORDER BY mean_exec_time;"
64 | ),
65 | median: await db.query(
66 | "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY mean_exec_time) AS median FROM pg_stat_statements WHERE query LIKE '%INSERT%';"
67 | ),
68 | mean: await db.query(
69 | "SELECT avg(mean_exec_time) AS averageQueryTime FROM pg_stat_statements WHERE query LIKE '%INSERT%';"
70 | ),
71 | slowestQueries: await db.query(
72 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%INSERT%' ORDER BY mean_exec_time DESC LIMIT $1;",
73 | [slowQueryNumber]
74 | ),
75 | };
76 | res.locals.result.insertTimes = insertQueries;
77 |
78 | const updateQueries: queryData = {
79 | all: await db.query(
80 | "SELECT * FROM pg_stat_statements WHERE query LIKE '%UPDATE%' ORDER BY mean_exec_time;"
81 | ),
82 | median: await db.query(
83 | "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY mean_exec_time) AS median FROM pg_stat_statements WHERE query LIKE '%UPDATE%';"
84 | ),
85 | mean: await db.query(
86 | "SELECT avg(mean_exec_time) AS averageQueryTime FROM pg_stat_statements WHERE query LIKE '%UPDATE%';"
87 | ),
88 | slowestQueries: await db.query(
89 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%UPDATE%' ORDER BY mean_exec_time DESC LIMIT $1;",
90 | [slowQueryNumber]
91 | ),
92 | };
93 | res.locals.result.updateTimes = updateQueries;
94 |
95 | const deleteQueries: queryData = {
96 | all: await db.query(
97 | "SELECT * FROM pg_stat_statements WHERE query LIKE '%DELETE%' ORDER BY mean_exec_time;"
98 | ),
99 | median: await db.query(
100 | "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY mean_exec_time) AS median FROM pg_stat_statements WHERE query LIKE '%DELETE%';"
101 | ),
102 | mean: await db.query(
103 | "SELECT avg(mean_exec_time) AS averageQueryTime FROM pg_stat_statements WHERE query LIKE '%DELETE%';"
104 | ),
105 | slowestQueries: await db.query(
106 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%DELETE%' ORDER BY mean_exec_time DESC LIMIT $1;",
107 | [slowQueryNumber]
108 | ),
109 | };
110 | res.locals.result.deleteTimes = deleteQueries;
111 |
112 | return next();
113 | } catch (error) {
114 | console.log('ERROR in databaseController.queryTimes: ', error);
115 | res.locals.result.allTimes = null;
116 | res.locals.result.selectTimes = null;
117 | res.locals.result.insertTimes = null;
118 | res.locals.result.updateTimes = null;
119 | res.locals.result.deleteTimes = null;
120 | return next();
121 | }
122 | },
123 |
124 | // this controller returns the number of rows that a given query touches, where the number is greater than 10
125 | numOfRows: async (req, res, next) => {
126 | const db = res.locals.dbConnection;
127 | // WIP: this number will come from the front end and currently defaults to 5
128 | const userProvided = req.body.numOfRowsNumber;
129 | const rowsNumber = userProvided || 10;
130 | try {
131 | const quantOfRows = await db.query(
132 | 'SELECT query, rows FROM pg_stat_statements WHERE rows > $1;',
133 | [rowsNumber]
134 | );
135 | res.locals.result.numOfRows = quantOfRows.rows;
136 | return next();
137 | } catch (error) {
138 | console.log('ERROR in databaseController.numOfRows: ', error);
139 | res.locals.result.numOfRows = null;
140 | return next();
141 | }
142 | },
143 |
144 | // this controller returns mean execution time for top queries of each query type
145 | topCalls: async (req, res, next) => {
146 | const db = res.locals.dbConnection;
147 | // WIP: this number will come from the front end and currently defaults to 5
148 | const userProvided = req.body.topCallsNumber;
149 | const callsNumber = userProvided || 5;
150 | try {
151 | const topAllCalls = await db.query(
152 | 'SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY calls DESC LIMIT $1;',
153 | [callsNumber]
154 | );
155 | const topSelectCalls = await db.query(
156 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%SELECT %' ORDER BY calls DESC LIMIT $1;",
157 | [callsNumber]
158 | );
159 | const topInsertCalls = await db.query(
160 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%INSERT %' ORDER BY calls DESC LIMIT $1;",
161 | [callsNumber]
162 | );
163 | const topDeleteCalls = await db.query(
164 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%DELETE %' ORDER BY calls DESC LIMIT $1;",
165 | [callsNumber]
166 | );
167 | const topUpdateCalls = await db.query(
168 | "SELECT query, mean_exec_time FROM pg_stat_statements WHERE query LIKE '%UPDATE %' ORDER BY calls DESC LIMIT $1;",
169 | [callsNumber]
170 | );
171 | res.locals.result.avgTimeTopAllCalls = topAllCalls.rows;
172 | res.locals.result.avgTimeTopSelectCalls = topSelectCalls.rows;
173 | res.locals.result.avgTimeTopInsertCalls = topInsertCalls.rows;
174 | res.locals.result.avgTimeTopDeleteCalls = topDeleteCalls.rows;
175 | res.locals.result.avgTimeTopUpdateCalls = topUpdateCalls.rows;
176 | return next();
177 | } catch (error) {
178 | console.log('ERROR in databaseController.topCalls: ', error);
179 | res.locals.result.averageQueryTime = null;
180 | res.locals.result.avgTimeTopSelectCalls = null;
181 | res.locals.result.avgTimeTopInsertCalls = null;
182 | res.locals.result.avgTimeTopDeleteCalls = null;
183 | res.locals.result.avgTimeTopUpdateCalls = null;
184 | return next();
185 | }
186 | },
187 |
188 | // this controller pulls the database-wide statistics for a specified database
189 | dbStats: async (req, res, next) => {
190 | const db = res.locals.dbConnection;
191 | const data = req.body.uri;
192 | // grab database name from req.body string, database assignment contigent upon db setup via uri || manual input
193 | const dataBase =
194 | data.split('.com/')[1] ||
195 | data.split('5432/').pop().split('/')[0].replace(/\s/g, '');
196 | try {
197 | const dbOverview = await db.query(
198 | 'SELECT * FROM pg_stat_database WHERE datname = $1;',
199 | [dataBase]
200 | );
201 | res.locals.result.dbStats = dbOverview.rows;
202 | res.locals.result.conflicts = dbOverview.rows[0].conflicts;
203 | res.locals.result.deadlocks = dbOverview.rows[0].deadlocks;
204 | res.locals.result.transactionsCommitted = dbOverview.rows[0].xact_commit;
205 | res.locals.result.rolledBackTransactions =
206 | dbOverview.rows[0].xact_rollback;
207 | return next();
208 | } catch (error) {
209 | console.log('ERROR in databaseController.dbStats: ', error);
210 | res.locals.result.dbStats = null;
211 | res.locals.result.conflicts = null;
212 | res.locals.result.deadlocks = null;
213 | res.locals.result.transactionsCommitted = null;
214 | res.locals.result.rolledBackTransactions = null;
215 | return next();
216 | }
217 | },
218 |
219 | // this controller calculates and returns the cache hit ratio for the database
220 | cacheHitRatio: async (req, res, next) => {
221 | const db = res.locals.dbConnection;
222 | const queryString =
223 | 'SELECT sum(heap_blks_read) AS heap_read, sum(heap_blks_hit) AS heap_hit, sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) AS ratio FROM pg_statio_user_tables;';
224 | try {
225 | const cacheHitRate = await db.query(queryString);
226 | res.locals.result.cacheHitRatio = cacheHitRate.rows;
227 | return next();
228 | } catch (error) {
229 | console.log('ERROR in databaseController.cacheHitRatio: ', error);
230 | res.locals.result.cacheHitRatio = null;
231 | return next();
232 | }
233 | },
234 |
235 | // this controller interfaces with db_stat_activity
236 | statActivity: async (req, res, next) => {
237 | const db = res.locals.dbConnection;
238 | const data = req.body.uri;
239 | // grab database name from req.body string, database assignment contigent upon db setup via uri || manual input
240 | const dataBase =
241 | data.split('.com/')[1] ||
242 | data.split('5432/').pop().split('/')[0].replace(/\s/g, '');
243 | try {
244 | const dbOverview = await db.query(
245 | "SELECT * FROM pg_stat_activity WHERE datname = $1 and state = 'active';",
246 | [dataBase]
247 | );
248 | res.locals.result.activeSessions = dbOverview.rowCount;
249 | return next();
250 | } catch (error) {
251 | console.log('ERROR in databaseController.statActivity: ', error);
252 | res.locals.result.activeSessions = null;
253 | return next();
254 | }
255 | },
256 | };
257 |
258 | //SELECT sum(numbackends) FROM pg_stat_database;
259 |
260 | export default databaseController;
261 |
--------------------------------------------------------------------------------
/server/routes/metricApi.ts:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 | import connectController from '../controllers/connectController';
3 | import databaseController from '../controllers/databaseController';
4 |
5 | const router = Router();
6 |
7 | // check if database being added is valid
8 | // WIP
9 | router.post(
10 | '/uri',
11 | connectController.connectDB,
12 | connectController.createExtension,
13 | databaseController.dbStats,
14 | (req, res) => {
15 | console.log(res.locals);
16 | return res.status(200).json(res.locals);
17 | }
18 | );
19 |
20 | // delivers metrics from database
21 | // notice: the database controllers will console log, but not return errors so that individual charts can render
22 | router.post(
23 | '/queryMetrics',
24 | connectController.connectDB,
25 | connectController.createExtension,
26 | databaseController.queryTimes,
27 | databaseController.numOfRows,
28 | databaseController.topCalls,
29 | databaseController.dbStats,
30 | databaseController.cacheHitRatio,
31 | databaseController.statActivity,
32 | (req, res) => {
33 | return res.status(200).json(res.locals.result);
34 | }
35 | );
36 |
37 | // this should reflect any method that would need to update based on user input
38 | // WIP: not currently in use but ready to be connected to front end based on user selection
39 | router.put(
40 | '/queryMetrics',
41 | databaseController.numOfRows,
42 | databaseController.queryTimes,
43 | databaseController.topCalls,
44 | (req, res) => {
45 | return res.status(200).json(res.locals.result);
46 | }
47 | );
48 |
49 | export default router;
50 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import metricAPI from './routes/metricApi';
3 | import express, {
4 | json,
5 | urlencoded,
6 | Request,
7 | Response,
8 | NextFunction,
9 | } from 'express';
10 |
11 | const app = express();
12 |
13 | const PORT = process.env.PORT || '3000';
14 |
15 | app.use(json());
16 | app.use(urlencoded({ extended: true }));
17 |
18 | // route for all Postgres Metrics
19 | app.use('/api', metricAPI);
20 |
21 | // serves static files in production mode
22 | if (process.env.NODE_ENV !== 'development') {
23 | app.use('/build', express.static(path.join(__dirname, '../build')));
24 | app.get('/*', (req, res) => {
25 | return res
26 | .status(200)
27 | .sendFile(path.join(__dirname, '../build/index.html'));
28 | });
29 | }
30 |
31 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
32 | const defaultError = {
33 | log: 'Express error handler caught unknown middleware error',
34 | status: 500,
35 | message: { Error: 'An error occurred' },
36 | };
37 |
38 | const errorObj = Object.assign({}, defaultError, err);
39 | return res.status(errorObj.status).json(errorObj.message);
40 | });
41 |
42 | const server = app.listen(PORT, () => {
43 | console.log(`Server started on ${PORT}...`);
44 | });
45 |
46 | module.exports = server;
47 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "commonjs",
5 | "noImplicitAny": true,
6 | "removeComments": true,
7 | "preserveConstEnums": true,
8 | "sourceMap": true,
9 | "declaration": true,
10 | "moduleResolution": "node",
11 | "esModuleInterop": true,
12 | "outDir": "./dist",
13 | "rootDir": ".",
14 | "allowJs": true,
15 | "lib": ["DOM"]
16 | },
17 | "include": ["client", "import-images.d.ts"]
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const path = require('path');
3 | const HTMLWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | // dictates development or production environment
7 | mode: process.env.NODE_ENV,
8 | // where Webpack starts constructing bundle.js
9 | entry: './client/index.tsx',
10 | // where bundle.js will be placed after compiling
11 | output: {
12 | path: path.resolve(__dirname, 'build'),
13 | filename: 'bundle.js',
14 | publicPath: '/build',
15 | },
16 | // used to make a copy of index.html file for serving in dev mode
17 | plugins: [
18 | new HTMLWebpackPlugin({
19 | template: './client/index.html',
20 | favicon: './client/assets/icons8-hexagon-office-32.png',
21 | }),
22 | ],
23 | // details for bundle transpiling
24 | module: {
25 | rules: [
26 | {
27 | // transpile js or jsx files
28 | test: /\.jsx?$/,
29 | // node_modules already transpiled
30 | exclude: /node_modules/,
31 | use: {
32 | loader: 'babel-loader',
33 | options: {
34 | // env is ES6 to ES5, react is jsx to js (order matters)
35 | presets: ['@babel/preset-env', '@babel/preset-react'],
36 | },
37 | },
38 | },
39 | {
40 | test: /\.tsx?$/,
41 | use: 'ts-loader',
42 | exclude: /node_modules/,
43 | },
44 | {
45 | // transpile scss or sass
46 | test: /\.s[ac]ss$/i,
47 | // creates style nodes from JS strings, translates CSS into Common JS, Compiles SASS to CSS (order matters)
48 | use: ['style-loader', 'css-loader', 'sass-loader'],
49 | },
50 | {
51 | // transpile css
52 | test: /\.css$/i,
53 | use: ['style-loader', 'css-loader'],
54 | },
55 | {
56 | test: /\.(jpe?g|png|gif|svg)$/i,
57 | use: [
58 | {
59 | loader: 'url-loader',
60 | options: {
61 | name: '[name].[ext]',
62 | },
63 | },
64 | ],
65 | },
66 | ],
67 | },
68 | resolve: {
69 | extensions: ['.tsx', '.ts', '.js', '.jsx'],
70 | modules: [path.join(__dirname, './node_modules')],
71 | },
72 | // server allows for hot module reloading (HMR) in dev mode
73 | devServer: {
74 | static: {
75 | // tells webpack dev server where to serve static content
76 | directory: path.resolve(__dirname, 'build'),
77 | // path must have wildcard so that all routes are served
78 | publicPath: '/*',
79 | },
80 | // allows backend requests to 8080 to be routed to 3000 automatically
81 | proxy: {
82 | '/api': 'http://localhost:3000',
83 | },
84 | },
85 | };
86 |
--------------------------------------------------------------------------------