├── .env.example
├── .eslintrc.cjs
├── .gitignore
├── .husky
├── commit-msg
├── pre-commit
└── pre-push
├── CONTRIBUTING.md
├── Dockerfile.grafana
├── README.md
├── T3_README.md
├── __tests__
└── jest
│ └── routes.test.ts
├── cypress.config.ts
├── cypress
├── fixtures
│ └── example.json
├── integration
│ ├── components
│ │ ├── GraphCard.cy.tsx
│ │ ├── HamburgerMenu.cy.tsx
│ │ ├── Header.cy.tsx
│ │ ├── InputQuery.cy.tsx
│ │ ├── LoadingBar.cy.tsx
│ │ ├── Popup.cy.tsx
│ │ ├── modal
│ │ │ ├── DBCredentials.cy.tsx
│ │ │ ├── DBModal.cy.tsx
│ │ │ ├── DBSelection.cy.tsx
│ │ │ ├── GrafanaCredentials.cy.tsx
│ │ │ └── ModalFormInput.cy.tsx
│ │ └── queryLog
│ │ │ ├── QueryLog.cy.tsx
│ │ │ └── QueryLogItem.cy.tsx
│ ├── containers
│ │ ├── DashboardContainer.cy.tsx
│ │ ├── MainContainer.cy.tsx
│ │ ├── QueryContainer.cy.tsx
│ │ └── SideBarContainer.cy.tsx
│ └── pages
│ │ ├── _appMyApp.cy.tsx
│ │ ├── aboutAbout.cy.tsx
│ │ ├── faqFAQ.cy.tsx
│ │ └── homepageHomepage.cy.tsx
├── support
│ ├── commands.ts
│ ├── component-index.html
│ └── component.ts
└── tsconfig.json
├── docker-compose.yml
├── grafana-build.sh
├── grafana.sh
├── grafana
└── grafana.ini
├── jest.config.ts
├── landing
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── public
│ └── favicon.ico
├── src
│ ├── components
│ │ ├── About.tsx
│ │ ├── FAQ.tsx
│ │ ├── Features.tsx
│ │ ├── Footer.tsx
│ │ ├── HamburgerMenu.tsx
│ │ ├── Header.tsx
│ │ ├── Hero.tsx
│ │ └── Team.tsx
│ ├── hooks
│ │ └── getWindowDimensions.tsx
│ └── pages
│ │ └── index.tsx
├── styles
│ └── landingstyles.css
├── tailwind.config.ts
└── tsconfig.json
├── next.config.mjs
├── nodemon.json
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── prisma
├── schema.prisma
└── seed.ts
├── public
├── assets
│ ├── Demo_connectDB.gif
│ ├── Demo_queryInput.gif
│ ├── Zoom Background.jpg
│ ├── backgroundContainer.png
│ ├── github-mark-white-.png
│ ├── linkedin-icon-update.png
│ ├── logo-128.png
│ ├── logo-32.png
│ ├── logo-64.png
│ ├── logo-full-background-color.png
│ ├── logo-full-bg.png
│ └── logo-full-no-bg.png
└── favicon.ico
├── src
├── components
│ ├── AuthShowcase.tsx
│ ├── Button.tsx
│ ├── DBConnect.tsx
│ ├── GraphCard.tsx
│ ├── HamburgerMenu.tsx
│ ├── Header.tsx
│ ├── InputQuery.tsx
│ ├── LoadingBar.tsx
│ ├── Popup.tsx
│ ├── modal
│ │ ├── DBCredentials.tsx
│ │ ├── DBModal.tsx
│ │ ├── DBSelection.tsx
│ │ ├── GrafanaCredentials.tsx
│ │ └── ModalFormInput.tsx
│ └── queryLog
│ │ ├── EditButton.tsx
│ │ ├── QueryLog.tsx
│ │ └── QueryLogItem.tsx
├── containers
│ ├── DashboardContainer.tsx
│ ├── MainContainer.tsx
│ ├── QueryContainer.tsx
│ └── SideBarContainer.tsx
├── env.mjs
├── pages
│ ├── [id].tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── about.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth].ts
│ │ ├── restricted.tsx
│ │ └── trpc
│ │ │ └── [trpc].ts
│ ├── contact.tsx
│ ├── faq.tsx
│ ├── homepage.tsx
│ └── index.tsx
├── server
│ ├── .env.example
│ ├── auth.ts
│ ├── controllers
│ │ ├── connectionController.ts
│ │ ├── dashBoardHelper.ts
│ │ ├── grafanaController.ts
│ │ └── pgQueryHelper.ts
│ ├── db.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── routers
│ │ └── apiRouter.ts
│ └── server.ts
├── styles
│ └── globals.css
├── types
│ └── types.ts
└── utils
│ └── .gitkeep
├── tailwind.config.ts
├── tsconfig.build.json
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to
2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date
3 | # when you add new variables to `.env`.
4 |
5 | # This file will be committed to version control, so make sure not to have any
6 | # secrets in it. If you are cloning this repo, create a copy of this file named
7 | # ".env" and populate it with your secrets.
8 |
9 | ##################################################
10 | # When adding additional environment variables, #
11 | # the schema in "/src/env.mjs" #
12 | # should be updated accordingly. #
13 | ##################################################
14 |
15 | NODE_ENV="development" # development | test | production
16 |
17 | # Prisma
18 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env
19 | # Prisma
20 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env
21 | DATABASE_URL_PRISMA=''
22 | DATABASE_URL_NODE=''
23 |
24 | # Next Auth
25 | # You can generate a new secret on the command line with:
26 | # openssl rand -base64 32
27 | # https://next-auth.js.org/configuration/options#secret
28 | # Not providing any secret or NEXTAUTH_SECRET will throw an error in production.
29 | # Leaving this empty in development won't throw an error, but may result in JWT decryption errors.
30 | NEXTAUTH_SECRET=""
31 | NEXTAUTH_URL="http://localhost:3000"
32 |
33 | # Next Auth Github Provider
34 | GITHUB_ID=""
35 | GITHUB_SECRET=""
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const path = require("path");
3 |
4 | /** @type {import("eslint").Linter.Config} */
5 | const config = {
6 | overrides: [
7 | {
8 | extends: [
9 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
10 | ],
11 | files: ["*.ts", "*.tsx"],
12 | parserOptions: {
13 | project: path.join(__dirname, "tsconfig.json"),
14 | },
15 | },
16 | ],
17 | parser: "@typescript-eslint/parser",
18 | parserOptions: {
19 | project: path.join(__dirname, "tsconfig.json"),
20 | },
21 | plugins: ["@typescript-eslint"],
22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
23 | rules: {
24 | "@typescript-eslint/consistent-type-imports": [
25 | "warn",
26 | {
27 | prefer: "type-imports",
28 | fixStyle: "inline-type-imports",
29 | },
30 | ],
31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
32 | },
33 | };
34 |
35 | module.exports = config;
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | */node_modules
6 | **/node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # database
14 | /prisma/db.sqlite
15 | /prisma/db.sqlite-journal
16 |
17 | # next.js
18 | /.next/
19 | /out/
20 | next-env.d.ts
21 |
22 | # production
23 | /build
24 |
25 | # misc
26 | .DS_Store
27 | *.pem
28 |
29 | # debug
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 | .pnpm-debug.log*
34 |
35 | # local env files
36 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
37 | .env
38 | .env*.local
39 |
40 | # vercel
41 | .vercel
42 |
43 | # typescript
44 | *.tsbuildinfo
45 |
46 | # binaries
47 | grafana-agent*
48 | *config.y*ml
49 |
50 |
51 | landingbuild/
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | # npx --no --commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | # Exit if in production
5 | # More specifically, most Continuous Integration Servers set a CI environment variable.
6 | # The following detects if this script is running in a CI, early exit
7 | [ -n "$CI" ] && exit 0
8 |
9 | # Grab current name of branch
10 | LOCAL_BRANCH_NAME="$(git rev-parse --abbrev-ref HEAD)"
11 |
12 | # Branch name regex match
13 | VALID_BRANCH_REGEX='^((bug|docs|fix|feat|merge|test|wip)\/[a-zA-Z0-9\-]+)$'
14 |
15 | ERROR_MSG="\nERROR: There is something wrong with your branch name.\nBranch names must adhere to this contract:\n\n$VALID_BRANCH_REGEX\n\nDue to this conflict, your commit has been rejected. Please rename your branch to a valid name and try again."
16 |
17 | FIX_MSG="You can rename your current working branch with:\ngit branch --move \$OLD_NAME \$NEW_NAME\ngit push --set-upstream \$REMOTE \$NEW_NAME\ngit branch -a\n\nAnd then remove the old branch on remote:\ngit push \$REMOTE --delete \$OLD_NAME\n"
18 |
19 | # Rejects commit if current branch name does not match regex pattern
20 | echo 'Linting branch name...';
21 | if [[ ! $LOCAL_BRANCH_NAME =~ $VALID_BRANCH_REGEX ]]; then
22 | # Print error
23 | echo "$ERROR_MSG\n\n"
24 | # Optional: Error fix suggestion
25 | echo "$FIX_MSG"
26 | # Abort commit
27 | exit 1
28 | fi
29 | echo 'OK'
30 |
31 | echo 'Formatting staged files...';
32 | # Grab all files in staging
33 | FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
34 |
35 | # If staging is empty, abort commit
36 | [ -z "$FILES" ] && echo 'No staged files, aborting...' && exit 0
37 |
38 | # Format all staged files with Prettier
39 | echo "$FILES" | xargs ./node_modules/.bin/prettier --ignore-unknown --write
40 |
41 | echo 'Adding formatted files back to staging...';
42 |
43 | echo "$FILES" | xargs git add
44 |
45 | echo 'Pre-flight complete.\nCommitting...'
46 |
47 | exit 0
48 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | # Prevents force-pushing to main or dev
5 | BRANCH=$(git rev-parse --abbrev-ref HEAD)
6 | PUSH_COMMAND=$(ps -ocommand= -p $PPID)
7 | PROTECTED_BRANCHES="^(main|dev|release\/*|patch\/*)"
8 | FORCE_PUSH="force|delete|-f"
9 | if [[ "$BRANCH" =~ $PROTECTED_BRANCHES && "$PUSH_COMMAND" =~ $FORCE_PUSH ]]; then
10 | echo "WARN: Prevented force-push to protected branch \"$BRANCH\" by pre-push hook"
11 | exit 1
12 | fi
13 |
14 | # Tests must pass before pushing
15 | # npm test
16 |
17 | exit 0
18 |
19 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | The QueryIQ team really appreciates contributions from the community. If you're interested in contributing, please see below:
4 |
5 | ## How Can I Contribute?
6 | - If you have any suggestions to improve our project, please fork this repository, create a pull request and describe what your contribution or issue is.
7 |
8 | ## Spin up the dev server
9 | 1. Pull the repo down locally
10 | 2. Ensure `./grafana.sh` is executable (`chmod +x ./grafana.sh` if it isn't)
11 | 3. `docker-compose up & npm run serve:dev`
12 | 4. Default credentials for your dockerized grafana and postgres server:
13 | - Grafana:
14 | ```
15 | User: admin
16 | Pass: admin
17 | Port: 3000
18 | ```
19 | - Postgres:
20 | ```
21 | DB Name: pgdev
22 | User: admin
23 | Pass: postgres
24 | URL: localhost:5432
25 | Server: localhost:5432
26 | ```
27 |
28 |
29 |
30 | ## Iteration Roadmap
31 |
32 | [] Reimplement data-vis service with home-rolled solution, migrate away from Grafana.
33 | [] Add functionality to connect to more than one database at a time.
34 | [] Add functionality to connect to other types of databases such as MySQL, SQLite.
35 | [] Implement plug-and-play containerization that requires less config.
36 | [] Implement solution that automatically searches for active databases on local machine `nmap -sV localhost -p- | grep ${DB_VENDER}`, or `netstat -an | grep docker` or `ps waux | grep docker`.
37 | [] Integrate Prometheus to allow for time series databases, alerting, and more customization on query and visualization
38 | [WIP] Integrate Open AI as means of suggesting optimized queries, to replace less performant queries
39 | [x] Containerize Grafana, possibly Prometheus, with Docker to enhance overall consistency across environments and operational efficiency
40 | [] Present modal interpretations of performance metrics that succinctly describe the issues and provide actionable recommendations for resolution
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Dockerfile.grafana:
--------------------------------------------------------------------------------
1 | FROM grafana/grafana-enterprise:latest
2 |
3 | COPY ./grafana/grafana.ini /etc/grafana/grafana.ini
4 |
5 | EXPOSE 3000
6 |
7 | CMD ["grafana-server", "--config=/etc/grafana/grafana.ini", "--homepath=/usr/share/grafana", "--packaging=docker", "--pidfile=/var/run/grafana/grafana-server.pid", "cfg:default.paths.data=/var/lib/grafana", "cfg:default.paths.logs=/var/log/grafana", "cfg:default.paths.plugins=/var/lib/grafana/plugins"]
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 | 
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 
28 | 
29 | 
30 | 
31 | 
32 | 
33 | 
34 | 
35 | 
36 | 
37 | 
38 | 
39 | 
40 | 
41 | 
42 | 
43 | 
44 |
45 |
46 |
47 |
48 |
49 |
50 | # Table of Contents
51 |
52 |
53 |
54 |
55 | About
56 | Key Features
57 | Installation
58 | Contributing
59 | Authors
60 |
61 |
62 |
63 |
64 |
65 |
66 | # About
67 | QueryIQ is a developer-friendly application designed to transform the process of analyzing and optimizing PostgreSQL databases. With its features, QueryIQ enables developers to gain valuable insights by creating data visualization dashboards based on database performance and query metrics.
68 |
69 | Visit our website: query-iq.com !
70 |
71 | ## Demo
72 |
73 |
74 | Connecting Query IQ to user database to receive health & performance metrics:
75 |
76 |
77 |
78 |
79 | User inputting an arbitrary query to receive query execution stats:
80 |
81 |
82 |
83 |
84 |
85 |
86 | # Key Features:
87 |
88 | ### ➮ PostgresQL Support
89 |
90 | Easily manage your postgresQL connection, health, and performance metrics
91 |
92 | ### ➮ Grafana Integration
93 |
94 | Query IQ simplifies managing your Grafana instance by creating data sources, customizing dashboards, and imbedding graphs within the application. Users also have the option to remove their data sources and dashboards as needed.
95 |
96 | ### ➮ Overall metrics on database health including:
97 |
98 | - Queries with the Longest Running Queries
99 | - Queries with the Highest Average Execution Time
100 | - Queries with the Highest Memory Usage
101 | - Row Counts per Table
102 | - Index Scans by Table
103 | - Total of Table Size and Index Size
104 | - Cache-hit Ratio
105 | - All databases connected to that server by size
106 | - Open Connections
107 |
108 | ### ➮ Overall metrics on multiple arbitrary query inputs including:
109 |
110 | - Query plan by aggregated with actual time, rows, and width
111 | - Sequence scan with actual time, rows, and width
112 | - Planning time
113 | - Execution time
114 |
115 | ### ➮ Secured authorization through Google Oauth with required login
116 | Users are required to login with Google Oauth for authorization prior to using the application.
117 |
118 | ### ➮ Privacy and Security
119 |
120 | Privacy and security within QueryIQ is maintained through running in individual local server along with Grafana's local instance with authorization required. QueryIQ does not store any user data, most importantly including database connection information, usernames, and passwords. Data is maintained within Grafana's local instance with authorization required and access restricted by the client as needed.
121 |
122 | # Installation
123 |
124 | ## Prerequisites
125 |
126 | ➮ Go to the following link to download the latest version (10.0.1) of Grafana: https://grafana.com/docs/grafana/latest/setup-grafana/installation/
127 |
128 | ➮ Go to your grafana.ini configurations file and ensure you have the following configurations. Refer to the the link if you're having trouble locating the grafana.ini: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/
129 |
130 | allow_embedding = true
131 | auth.anonymous
132 | enabled = true
133 | org_name = <>
134 | org_role = Viewer
135 |
136 | ➮ Once you have Grafana installed, run the following command to start your Grafana local instance and ensure you've logged in successfully in the Grafana server
137 |
138 | - On OSX, `brew services restart grafana`, or by running the Grafana binary
139 |
140 | ➮ For the PostgreSQL database you connect to, ensure pg_stat_statements is enabled. Refer to the link for further details: https://virtual-dba.com/blog/postgresql-performance-enabling-pg-stat-statements/
141 |
142 | ## Install Query IQ
143 |
144 | 1. Clone this repo
145 | 2. `cd` into project directory
146 | 3. Run `npm install`
147 | 4. Run `npm run npm:fullInstall` to install packages nested within the project
148 | 5. Run `npm run serve:dev` to spin up the Next.js and Express server layers
149 | 6. Start Grafana (please see [Prerequisites](#Prerequisites))
150 |
151 | # How to Contribute
152 |
153 | - Please read [CONTRIBUTING.md](#) for details on how to contribute.
154 |
155 |
156 | # Authors
157 |
158 | | Developed By | Github | LinkedIn |
159 | | :-: | :-: |:-: |
160 | |Connor Dillon |[](https://github.com/connoro7) |[](https://www.linkedin.com/in/connor-dillon/)|
161 | | Khaile Tran |[](https://github.com/khailetran) | [](https://www.linkedin.com/in/khailetran/)|
162 | |Johanna Cameron|[](https://github.com/jojecameron)|[](https://www.linkedin.com/in/johanna-cameron/) |
163 | |Dean Biscocho|[](https://github.com/deanbiscocho)|[](https://www.linkedin.com/in/deanbiscocho/)|
164 | |Alan Beck |[](https://github.com/KAlanBeck)| [](https://www.linkedin.com/in/k-alan-beck/) |
165 |
166 | ## Show Your Support
167 |
168 | Please ⭐️ this project if you enjoy our tool, thank you so much!
169 |
--------------------------------------------------------------------------------
/T3_README.md:
--------------------------------------------------------------------------------
1 | # Create T3 App
2 |
3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
4 |
5 | ## What's next? How do I make an app with this?
6 |
7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
8 |
9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
10 |
11 | - [Next.js](https://nextjs.org)
12 | - [NextAuth.js](https://next-auth.js.org)
13 | - [Prisma](https://prisma.io)
14 | - [Tailwind CSS](https://tailwindcss.com)
15 | - [tRPC](https://trpc.io)
16 |
17 | ## Learn More
18 |
19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
20 |
21 | - [Documentation](https://create.t3.gg/)
22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
23 |
24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
25 |
26 | ## How do I deploy this?
27 |
28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
29 |
--------------------------------------------------------------------------------
/__tests__/jest/routes.test.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import request from "supertest";
4 | import assert from "assert";
5 | import { exec, ChildProcess } from 'child_process';
6 |
7 | const server = "http://localhost:3001";
8 | let serverProcess: ChildProcess;
9 |
10 | beforeAll(async () => {
11 | console.log('Starting the server...');
12 | const startServer = 'npm run serve:dev';
13 | serverProcess = exec(startServer);
14 | await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for the server to start (adjust the delay as needed)
15 | });
16 |
17 | afterAll(() => {
18 | console.log('Stopping the server...');
19 | serverProcess.kill(); // Terminate the server process (you may need to customize this logic to gracefully stop your server)
20 | });
21 |
22 | describe("Datasource and Dashboard creation", () => {
23 | describe("POST", () => {
24 | it("responds with 200 status and application/json content type", async () => {
25 | await request(server)
26 | .post("/api/connect")
27 | .expect("Content-Type", /application\/json/)
28 | .expect(200);
29 | });
30 | });
31 | });
32 |
33 | describe("Error catch route", () => {
34 | it("should respond with a 404 error and 'page not found'", async () => {
35 | const response = await request(server).get("/wrongaddress");
36 | expect(response.status).toBe(404);
37 | expect(response.text).toBe("Page not found");
38 | });
39 | });
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | component: {
5 | devServer: {
6 | framework: "next",
7 | bundler: "webpack",
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/integration/components/GraphCard.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import GraphCard from '../../../src/components/GraphCard'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/HamburgerMenu.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import HamburgerMenu from './HamburgerMenu'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/Header.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from './Header'
3 |
4 | describe('', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount()
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/InputQuery.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import InputQuery from './InputQuery'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/LoadingBar.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LoadingBar from './LoadingBar'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/Popup.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Popup from './Popup'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/modal/DBCredentials.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DBCredentials from '../../../../src/components/modal/DBCredentials'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/modal/DBModal.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DBModal from './DBModal'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/modal/DBSelection.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DBSelection from './DBSelection'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/modal/GrafanaCredentials.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import GrafanaCredentials from './GrafanaCredentials'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/modal/ModalFormInput.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ModalFormInput from './ModalFormInput'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/queryLog/QueryLog.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import QueryLog from '../../../../src/components/queryLog/QueryLog'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/components/queryLog/QueryLogItem.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import QueryLogItem from './QueryLogItem'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/containers/DashboardContainer.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DashboardContainer from '../../../src/containers/DashboardContainer'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/containers/MainContainer.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MainContainer from './MainContainer'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/containers/QueryContainer.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import QueryContainer from './QueryContainer'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/containers/SideBarContainer.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SideBarContainer from './SideBarContainer'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/pages/_appMyApp.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MyApp from '../../../src/pages/_app'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/pages/aboutAbout.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import About from './about'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/pages/faqFAQ.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FAQ from './faq'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/integration/pages/homepageHomepage.cy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Homepage from './homepage'
3 |
4 | describe(' ', () => {
5 | it('renders', () => {
6 | // see: https://on.cypress.io/mounting-react
7 | cy.mount( )
8 | })
9 | })
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/component.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | import { mount } from 'cypress/react18'
23 |
24 | // Augment the Cypress namespace to include type definitions for
25 | // your custom command.
26 | // Alternatively, can be defined in cypress/support/component.d.ts
27 | // with a at the top of your spec.
28 | declare global {
29 | namespace Cypress {
30 | interface Chainable {
31 | mount: typeof mount
32 | }
33 | }
34 | }
35 |
36 | Cypress.Commands.add('mount', mount)
37 |
38 | // Example use:
39 | // cy.mount( )
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "isolatedModules": false,
6 | "types": ["cypress"]
7 | },
8 | "include": ["**/*.ts","**.spec.ts"]
9 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | grafana:
5 | image: grafana/grafana-enterprise:latest
6 | ports:
7 | - '3000:3000'
8 | db:
9 | image: postgres:latest
10 | ports:
11 | - '5432:5432'
12 | environment:
13 | POSTGRES_USER: admin
14 | POSTGRES_PASSWORD: postgres
15 | POSTGRES_DB: pgdev
16 |
--------------------------------------------------------------------------------
/grafana-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Color vars
4 | RED='\033[0;31m'
5 | GREEN='\033[0;32m'
6 | YELLOW='\033[0;33m'
7 | COLOR_OFF='\033[0m'
8 |
9 | # Ensure OS is macOS
10 | if [[ "$OSTYPE" != "darwin"* ]]; then
11 | echo "This script is only for macOS!";
12 | exit 1;
13 | fi
14 |
15 | # Ensure Intel chip
16 | if [[ "$HOSTTYPE" != "x86_64" ]]; then
17 | echo "This script is only for Intel chips!";
18 | exit 1;
19 | fi
20 |
21 | # Install Grafana Agent from binary if doesn't exist
22 | if [ ! -f ./grafana-agent-darwin-amd64 ]; then
23 | curl -O -L "https://github.com/grafana/agent/releases/latest/download/grafana-agent-darwin-amd64.zip";
24 | unzip "grafana-agent-darwin-amd64.zip";
25 | chmod a+x "grafana-agent-darwin-amd64";
26 | fi
27 |
28 | # Ensure .env file exists
29 | if [ ! -f ./.env ]; then
30 | echo "Please create a .env file with the following variables: GRAFANA_CLOUD_MACOS_AMD64_BINARY_API_KEY, GRAFANA_GCLOUD_HOSTED_METRICS_URL, GRAFANA_GCLOUD_HOSTED_METRICS_ID, GRAFANA_GCLOUD_HOSTED_LOGS_URL, GRAFANA_GCLOUD_HOSTED_LOGS_ID";
31 | exit 1;
32 | fi
33 |
34 | # Set environment variables
35 | ARCH="amd64"
36 | GCLOUD_HOSTED_METRICS_URL="$(grep GRAFANA_GCLOUD_HOSTED_METRICS_URL .env | cut -d '=' -f2-)"
37 | GCLOUD_HOSTED_METRICS_ID="$(grep GRAFANA_GCLOUD_HOSTED_METRICS_ID .env | cut -d '=' -f2-)"
38 | GCLOUD_SCRAPE_INTERVAL="60s"
39 | GCLOUD_HOSTED_LOGS_URL="$(grep GRAFANA_GCLOUD_HOSTED_LOGS_URL .env | cut -d '=' -f2-)"
40 | GCLOUD_HOSTED_LOGS_ID="$(grep GRAFANA_GCLOUD_HOSTED_LOGS_ID .env | cut -d '=' -f2-)"
41 | GCLOUD_RW_API_KEY="$(grep GRAFANA_CLOUD_MACOS_AMD64_BINARY_API_KEY .env | cut -d '=' -f2-)=="
42 |
43 | if [ -z "$GCLOUD_HOSTED_METRICS_URL" ] || [ -z "$GCLOUD_HOSTED_METRICS_ID" ] || [ -z "$GCLOUD_SCRAPE_INTERVAL" ] || [ -z "$GCLOUD_HOSTED_LOGS_URL" ] || [ -z "$GCLOUD_HOSTED_LOGS_ID" ] || [ -z "$GCLOUD_RW_API_KEY" ]; then
44 | echo "${RED}Please ensure all environment variables are set in .env file${COLOR_OFF}";
45 | exit 1;
46 | else
47 | echo "${GREEN}Environment variables correctly set in .env file, following warning is a race condition that is actually resolved.${COLOR_OFF}";
48 | /bin/sh -c "$(curl -fsSL https://storage.googleapis.com/cloud-onboarding/agent/scripts/grafanacloud-install-darwin.sh)"
49 |
50 | fi
51 |
52 | # Fetch config file from Grafana Cloud
53 |
54 |
55 | # Move config file to root dir
56 | # if ./grafana-config.yml does not exist, move from /usr/local/etc/grafana-agent/config.yaml
57 | if [ ! -f ./grafana-config.yml ]; then
58 | mv /usr/local/etc/grafana-agent/config.yml ./grafana-config.yml
59 | fi
60 |
61 | # Run the Agent if not currently running, otherwise kill and restart
62 | if lsof -Pi :12345 -sTCP:LISTEN -t >/dev/null ; then
63 | kill -9 $(lsof -t -i:12345) && ./grafana-agent-darwin-amd64 --config.file=./grafana-config.yml
64 | else
65 | ./grafana-agent-darwin-amd64 --config.file=./grafana-config.yml
66 | fi
67 |
--------------------------------------------------------------------------------
/grafana.sh:
--------------------------------------------------------------------------------
1 | docker run -d -p 3000:3000 --name=grafana \
2 | --volume grafana-storage:/var/lib/grafana \
3 | -e GF_SECURITY_ADMIN_USER=admin \
4 | -e GF_SECURITY_ADMIN_PASSWORD=password \
5 | grafana/grafana-enterprise
6 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/__tests__/**/*.test.ts'],
5 | verbose: true,
6 | forceExit: true,
7 | clearMocks: true
8 | };
9 |
--------------------------------------------------------------------------------
/landing/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("next").NextConfig} */
2 | const config = {
3 | reactStrictMode: true,
4 |
5 | /**
6 | * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config out.
7 | *
8 | * @see https://github.com/vercel/next.js/issues/41980
9 | */
10 | i18n: {
11 | locales: ["en"],
12 | defaultLocale: "en",
13 | },
14 | images: {
15 | remotePatterns: [
16 | {
17 | protocol: 'https',
18 | hostname: 'github.com'
19 | },
20 | {
21 | protocol: 'https',
22 | hostname: '**.githubusercontent.com'
23 | },
24 | {
25 | protocol: 'https',
26 | hostname: '**.discordapp.com'
27 | },
28 | ],
29 | },
30 | distDir: 'landingbuild',
31 | };
32 | export default config;
--------------------------------------------------------------------------------
/landing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "landing",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "landing:build": "next build",
8 | "landing:dev": "next dev -p 8888",
9 | "landing:lint": "next lint",
10 | "landing:start": "next start -p 8888"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "flowbite": "^1.7.0",
17 | "flowbite-react": "^0.5.0",
18 | "next": "^13.4.12",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-icons": "^4.10.1"
22 | },
23 | "devDependencies": {
24 | "postcss": "^8.4.27",
25 | "prettier": "^2.8.8",
26 | "prettier-plugin-tailwind": "^2.2.12"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/landing/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/landing/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | plugins: [require.resolve("prettier-plugin-tailwindcss")],
4 | semi: true,
5 | singleQuote: true,
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/landing/public/favicon.ico:
--------------------------------------------------------------------------------
1 | ��4�}�
--------------------------------------------------------------------------------
/landing/src/components/About.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 |
4 | const About: React.FC = () => {
5 | return (
6 | <>
7 |
8 |
9 |
Query IQ is a powerful developer tool that provides holistic insights on your SQL database.
10 | {/*
*/}
11 |
12 |
13 |
14 |
15 |
21 |
22 |
Database Health Metrics
23 |
24 | Monitor crucial metrics for your database such as query execution time, memory usage, cache-hit ratio, and more!
25 |
26 |
27 |
28 |
29 |
30 |
31 |
37 |
38 |
Customizable Query Log
39 |
40 | Keep tabs on the queries you make to your database, give them custom labels, and easily compare query performance.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 |
54 |
Query Performance
55 |
56 | Receive a granular level analysis of individual query performance by aggregating actual time, rows, and width.
57 |
58 |
59 |
60 |
61 |
62 |
63 | >
64 | );
65 | };
66 |
67 | export default About;
68 |
--------------------------------------------------------------------------------
/landing/src/components/FAQ.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FAQ = () => {
4 | return (
5 | <>
6 |
7 |
8 |
9 |
10 | FAQ
11 |
12 |
13 | Below you'll find the answers to some frequently asked questions
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Q.
23 |
24 |
25 | A.
26 |
27 |
28 |
29 |
30 |
31 | What are performance metrics for SQL database queries?
32 |
33 |
34 |
35 |
36 | Performance metrics for SQL database queries are measurements used to assess the efficiency and
37 | effectiveness of query execution. They include metrics such as query execution time, query throughput,
38 | CPU usage, memory usage, disk I/O, and more.
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Q.
49 |
50 |
51 | A.
52 |
53 |
54 |
55 |
56 |
57 | Why do performance metrics for database queries matter?
58 |
59 |
60 |
61 |
62 | Performance metrics for database queries matter because
63 | they provide insights into system efficiency, user
64 | experience, scalability, cost optimization,
65 | troubleshooting, optimization opportunities, SLA
66 | compliance, and enable continuous improvement. Monitoring
67 | and analyzing these metrics help maintain a
68 | well-performing database system and ensure that it meets
69 | the needs of users and applications.
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | Q.
79 |
80 |
81 | A.
82 |
83 |
84 |
85 |
86 |
87 | What is query execution time, and how can I optimize it?
88 |
89 |
90 |
91 |
92 | Query execution time refers to the duration it takes for a
93 | query to complete. To optimize query execution time, you
94 | can consider strategies such as indexing the appropriate
95 | columns, rewriting or optimizing the query logic,
96 | denormalizing data for better performance, or tuning the
97 | database configuration.
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | Q.
110 |
111 |
112 | A.
113 |
114 |
115 |
116 |
117 |
118 | What is query throughput, and how can I improve it?
119 |
120 |
121 |
122 |
123 | Query throughput refers to the number of queries that can
124 | be processed within a given timeframe. To improve query
125 | throughput, you can optimize the database schema, utilize
126 | caching mechanisms, distribute data across multiple
127 | servers, parallelize query execution, and implement query
128 | optimization techniques.
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | Q.
138 |
139 |
140 | A.
141 |
142 |
143 |
144 |
145 |
146 | What is query optimization, and how can I optimize my SQL
147 | queries?
148 |
149 |
150 |
151 |
152 | Query optimization involves improving the efficiency of
153 | SQL queries by selecting optimal execution plans. You can
154 | optimize SQL queries by using appropriate indexing,
155 | rewriting complex queries, avoiding unnecessary joins or
156 | subqueries, utilizing query hints, and analyzing and
157 | tuning the database configuration.
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | Q.
167 |
168 |
169 | A.
170 |
171 |
172 |
173 |
174 |
175 | What are some common SQL query performance issues and solutions?
176 |
177 |
178 |
179 |
180 | Common performance issues with SQL queries include slow
181 | query execution, high resource utilization, lack of
182 | indexing, inefficient join operations, and suboptimal
183 | query plans. You can address these issues by optimizing
184 | query logic, adding or modifying indexes, rewriting
185 | queries, and monitoring resource usage.
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | >
196 | );
197 | };
198 |
199 | export default FAQ;
200 |
--------------------------------------------------------------------------------
/landing/src/components/Features.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BiLogoPostgresql } from 'react-icons/bi';
3 | import { SiGrafana } from 'react-icons/si';
4 | import { GiPadlock } from 'react-icons/gi';
5 |
6 |
7 | const Features: React.FC = () => {
8 | return (
9 | <>
10 |
11 |
12 |
Features
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
PostgreSQL Support
21 |
22 | Effortlessly manage your PostgreSQL connection, health, and performance metrics with Query IQ. Simplify monitoring, performance optimization, and gain valuable insights.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Grafana Integration
30 |
31 | Query IQ simplifies managing your Grafana instance by creating data sources, pre-confgured dashboards, and embedded graphs within the application.
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Secure Authorization
39 |
40 | Seamlessly create an account using your existing GitHub credentials, ensuring a streamlined and secure registration process.
41 | Your data remains protected, and you can confidently access all the features with ease.
42 |
43 |
44 |
45 |
46 |
47 | >
48 | );
49 | }
50 |
51 | export default Features;
52 |
53 |
--------------------------------------------------------------------------------
/landing/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 |
4 | const Footer: React.FC = () => {
5 | return (
6 |
70 | );
71 | };
72 |
73 | export default Footer;
74 |
--------------------------------------------------------------------------------
/landing/src/components/HamburgerMenu.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { GiHamburgerMenu } from 'react-icons/gi';
3 | import Link from 'next/link';
4 |
5 | const HamburgerMenu = () => {
6 | const [isOpen, setIsOpen] = useState(false);
7 |
8 | const toggleMenu = () => {
9 | setIsOpen(!isOpen);
10 | };
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | {isOpen && (
18 |
43 | )}
44 |
45 | );
46 | };
47 |
48 | export default HamburgerMenu;
49 |
--------------------------------------------------------------------------------
/landing/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import React, { useState, useRef, useEffect } from 'react';
3 |
4 | const Header: React.FC = () => {
5 | // const [sticky, setSticky] = useState({ isSticky: false, offset: 0 });
6 | // const headerRef = useRef(null);
7 |
8 |
9 | // // handle scroll event
10 | // const handleScroll = (elTopOffset, elHeight) => {
11 | // if (window.scrollY > (elTopOffset + elHeight)) {
12 | // setSticky({ isSticky: true, offset: elHeight });
13 | // } else {
14 | // setSticky({ isSticky: false, offset: 0 });
15 | // }
16 | // };
17 |
18 | // // add/remove scroll event listener
19 | // useEffect(() => {
20 | // var header = headerRef.current.getBoundingClientRect();
21 | // const handleScrollEvent = () => {
22 | // handleScroll(header.top, header.height)
23 | // }
24 |
25 | // window.addEventListener('scroll', handleScrollEvent);
26 |
27 | // return () => {
28 | // window.removeEventListener('scroll', handleScrollEvent);
29 | // };
30 | // }, []);
31 |
32 | const headerStyle = {
33 | position: 'sticky',
34 | top: 0,
35 | bottom:0,
36 | zIndex: 999, // Optional: To ensure the header appears above other content
37 | background: 'linear-gradient(to bottom, #1F1F1F, transparent)', // Replace 'blue' with your desired background color
38 | padding: '8px',
39 | };
40 |
41 | return (
42 |
85 |
86 | );
87 | };
88 |
89 | export default Header;
90 |
--------------------------------------------------------------------------------
/landing/src/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Image from 'next/image';
3 | import useWindowDimensions from '../hooks/getWindowDimensions'
4 |
5 | const Hero = () => {
6 |
7 | // const { height, width } = useWindowDimensions();
8 | const width = '1000'
9 | return (
10 | <>
11 |
12 |
13 |
28 |
29 |
30 |
31 |
32 | Your database will thank you!
33 |
34 |
40 |
41 |
42 |
43 |
44 | >
45 | )
46 | }
47 |
48 | export default Hero
49 |
--------------------------------------------------------------------------------
/landing/src/components/Team.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 |
4 | const Team = () => {
5 | const teamMembers = [
6 | {
7 | image: 'https://github.com/KAlanBeck.png',
8 | name: 'Alan Beck',
9 | linkedin: 'https://www.linkedin.com/in/k-alan-beck/',
10 | github: 'https://github.com/KAlanBeck/',
11 | },
12 | {
13 | image: 'https://github.com/connoro7.png',
14 | name: 'Connor Dillon',
15 | linkedin: 'https://www.linkedin.com/in/connor-dillon/',
16 | github: 'https://github.com/connoro7/',
17 | },
18 | {
19 | image: 'https://github.com/deanbiscocho.png',
20 | name: 'Dean Biscocho',
21 | linkedin: 'https://www.linkedin.com/in/deanbiscocho/',
22 | github: 'https://github.com/deanbiscocho/',
23 | },
24 | {
25 | image: 'https://github.com/jojecameron.png',
26 | name: 'Johanna Cameron',
27 | linkedin: 'https://www.linkedin.com/in/johanna-cameron/',
28 | github: 'https://github.com/jojecameron/',
29 | },
30 | {
31 | image: 'https://github.com/khailetran.png',
32 | name: 'Khaile Tran',
33 | linkedin: 'https://www.linkedin.com/in/khailetran/',
34 | github: 'https://github.com/khailetran/',
35 | },
36 | // Add more team members as needed
37 | ];
38 | return (
39 | <>
40 |
41 |
42 |
Meet Our Team
43 |
44 | {teamMembers.map((member, index) => (
45 |
49 |
50 | src}
54 | alt="Profile"
55 | width={55}
56 | height={55}
57 | unoptimized
58 | />
59 |
60 |
61 |
62 | {member.name}
63 |
64 |
103 |
104 | ))}
105 |
106 |
107 |
108 | >
109 | );
110 | };
111 |
112 | export default Team;
113 |
--------------------------------------------------------------------------------
/landing/src/hooks/getWindowDimensions.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react';
3 |
4 | function getWindowDimensions() {
5 | const { innerWidth: width, innerHeight: height } = window;
6 |
7 | return {
8 | width,
9 | height
10 | };
11 | }
12 |
13 | export default function useWindowDimensions() {
14 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
15 |
16 | useEffect(() => {
17 | function handleResize() {
18 | setWindowDimensions(getWindowDimensions());
19 | }
20 |
21 | window.addEventListener('resize', handleResize);
22 | return () => window.removeEventListener('resize', handleResize);
23 | }, []);
24 |
25 | return windowDimensions;
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/landing/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { type NextPage } from 'next';
2 | import Image from 'next/image';
3 | import Head from 'next/head';
4 | import Hero from '../components/Hero';
5 | import Header from '../components/Header';
6 | import About from '../components/About';
7 | import FAQ from '../components/FAQ';
8 | import Team from '../components/Team';
9 | import Footer from '../components/Footer';
10 | import Features from '../components/Features'
11 | import '../../../src/styles/globals.css';
12 | // https://cdn.discordapp.com/attachments/1115285712292565056/1126317089712517190/QuIQ_query.gif
13 |
14 | const LandingHome: NextPage = () => {
15 |
16 |
17 | return (
18 | <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | >
31 | );
32 | };
33 |
34 | export default LandingHome;
--------------------------------------------------------------------------------
/landing/styles/landingstyles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | box-sizing: border-box;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | body {
12 | /*background-image: linear-gradient(to bottom, #0f0f0f, #1f1f1f);*/
13 | background: #1f1f1f;
14 | height: 100dvh;
15 | width: fit-content;
16 |
17 | }
18 |
19 | @layer utilities {
20 | .h-100dvh {
21 | height: 100dvh;
22 | }
23 | .mb-128 {
24 | margin-bottom: 36rem;
25 | }
26 | .outline-red {
27 | outline: 1px solid red;
28 | }
29 | .outline-blue {
30 | outline: 1px solid cyan;
31 | }
32 | .outline-green {
33 | outline: 1px solid chartreuse;
34 | }
35 | .outline-purple {
36 | outline: 1px solid magenta;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/landing/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from 'tailwindcss';
2 |
3 | const tailwindConfig: Config = {
4 | content: ['./src/**/*.{js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | 'reem-kufi': ['Reem Kufi', 'sans-serif'],
9 | },
10 | visibility: ['group-hover'],
11 | },
12 | },
13 | variants: {
14 | extend: {
15 | visibility: ['group-hover'],
16 | },
17 | },
18 | plugins: [],
19 | };
20 |
21 | export default tailwindConfig;
22 |
--------------------------------------------------------------------------------
/landing/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": false,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
3 | * for Docker builds.
4 | */
5 | import("./src/env.mjs");
6 |
7 | /** @type {import("next").NextConfig} */
8 | const config = {
9 | reactStrictMode: true,
10 |
11 | /**
12 | * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config out.
13 | *
14 | * @see https://github.com/vercel/next.js/issues/41980
15 | */
16 | i18n: {
17 | locales: ["en"],
18 | defaultLocale: "en",
19 | },
20 | tsconfigPath: "tsconfig.json",
21 | images: {
22 | remotePatterns: [
23 | {
24 | protocol: 'https',
25 | hostname: 'github.com',
26 |
27 | },
28 | ],
29 | },
30 | };
31 | export default config;
32 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src"],
3 | "ext": "ts,json",
4 | "ignore": [
5 | "src/**/*.spec.ts",
6 | "src/**/*.test.ts",
7 | "node_modules",
8 | "dist",
9 | "build",
10 | "src/**/*.d.ts",
11 | "src/**/*.map",
12 | "src/**/*.js.map",
13 | "src/**/*.spec.js",
14 | "src/**/*.test.js"
15 | ],
16 | "exec": "ts-node --transpile-only ./src/server/server.ts"
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-t3-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "npm:fullInstall": "npm install --prefix ./src/server && npm run prisma:generate",
7 | "next:build": "next build",
8 | "next:dev": "next dev -p 3333",
9 | "next:lint": "next lint",
10 | "next:start": "next start -p 3333",
11 | "express": "ts-node --transpile-only ./src/server/server.ts",
12 | "express:watch": "nodemon",
13 | "prisma:generate": "prisma generate",
14 | "prisma:push": "npx prisma db push",
15 | "lint": "next lint",
16 | "start": "next start",
17 | "prepare": "husky install && bash -c 'chmod ug+x .husky/*'",
18 | "prisma:studio": "npx prisma studio",
19 | "serve:dev": "concurrently \"npm run next:dev\" \"npm run express:watch\"",
20 | "cypress": "cypress open"
21 | },
22 | "dependencies": {
23 | "@next-auth/prisma-adapter": "^1.0.5",
24 | "@prisma/client": "^4.16.0",
25 | "@t3-oss/env-nextjs": "^0.3.1",
26 | "body-parser": "^1.20.2",
27 | "concurrently": "^8.2.0",
28 | "cors": "^2.8.5",
29 | "express": "^4.18.2",
30 | "helmet": "^7.0.0",
31 | "next": "^13.4.2",
32 | "next-auth": "^4.22.1",
33 | "pg": "^8.11.0",
34 | "react": "18.2.0",
35 | "react-dom": "18.2.0",
36 | "react-icons": "^4.10.1",
37 | "react-query": "^3.39.3",
38 | "uuid": "^9.0.0",
39 | "zod": "^3.21.4"
40 | },
41 | "devDependencies": {
42 | "@testing-library/cypress": "^9.0.0",
43 | "@testing-library/jest-dom": "^5.16.5",
44 | "@testing-library/react": "^14.0.0",
45 | "@types/cors": "^2.8.13",
46 | "@types/eslint": "^8.37.0",
47 | "@types/express": "^4.17.17",
48 | "@types/jest": "^29.5.2",
49 | "@types/node": "^18.16.18",
50 | "@types/pg": "^8.10.2",
51 | "@types/prettier": "^2.7.2",
52 | "@types/react": "^18.2.6",
53 | "@types/react-dom": "^18.2.4",
54 | "@types/supertest": "^2.0.12",
55 | "@types/testing-library__react": "^10.2.0",
56 | "@types/uuid": "^9.0.2",
57 | "@typescript-eslint/eslint-plugin": "^5.59.6",
58 | "@typescript-eslint/parser": "^5.59.6",
59 | "autoprefixer": "^10.4.14",
60 | "cypress": "^12.16.0",
61 | "eslint": "^8.40.0",
62 | "eslint-config-next": "^13.4.2",
63 | "husky": "^8.0.3",
64 | "eslint-plugin-cypress": "^2.13.3",
65 | "jest": "^29.5.0",
66 | "nodemon": "^2.0.22",
67 | "postcss": "^8.4.21",
68 | "prettier": "^2.8.8",
69 | "prettier-plugin-tailwindcss": "^0.2.8",
70 | "prisma": "^4.16.0",
71 | "supertest": "^6.3.3",
72 | "tailwindcss": "^3.3.0",
73 | "ts-jest": "^29.1.1",
74 | "ts-node": "^10.9.1",
75 | "typescript": "^5.1.3"
76 | },
77 | "ct3aMetadata": {
78 | "initVersion": "7.13.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | plugins: [require.resolve("prettier-plugin-tailwindcss")],
4 | semi: true,
5 | singleQuote: true,
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | previewFeatures = ["jsonProtocol"]
7 | }
8 |
9 | datasource db {
10 | provider = "mysql"
11 | // Further reading:
12 | // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
13 | // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
14 | url = env("DATABASE_URL_PRISMA")
15 | relationMode = "prisma"
16 | }
17 |
18 | // Necessary for Next auth
19 | model Account {
20 | id String @id @default(cuid())
21 | userId String
22 | type String
23 | provider String
24 | providerAccountId String
25 | refresh_token String? @db.Text
26 | refresh_token_expires_in Int?
27 | access_token String? @db.Text
28 | expires_at Int?
29 | token_type String?
30 | scope String?
31 | id_token String? @db.Text
32 | session_state String?
33 | user User @relation(fields: [userId], references: [id], onDelete: Cascade, map: "user_fk")
34 | PostgreSQL PostgreSQL[]
35 | MySQL MySQL[]
36 |
37 | @@unique([provider, providerAccountId])
38 | @@index([userId])
39 | }
40 |
41 | model Session {
42 | id String @id @default(cuid())
43 | sessionToken String @unique
44 | userId String
45 | expires DateTime
46 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
47 |
48 | @@index([userId])
49 | }
50 |
51 | model User {
52 | id String @id @default(cuid())
53 | name String?
54 | email String? @unique
55 | emailVerified DateTime?
56 | image String?
57 | accounts Account[]
58 | sessions Session[]
59 | PostgreSQL PostgreSQL[]
60 | MySQL MySQL[]
61 | }
62 |
63 | model VerificationToken {
64 | identifier String
65 | token String @unique
66 | expires DateTime
67 |
68 | @@unique([identifier, token])
69 | }
70 |
71 | model PostgreSQL {
72 | id String @id @default(cuid())
73 | userId String
74 | user User @relation(fields: [userId], references: [id])
75 | pg_username String // TODO: store securely
76 | pg_password String // TODO: store securely
77 | Account Account? @relation(fields: [accountId], references: [id])
78 | accountId String?
79 |
80 | @@index([userId])
81 | @@index([accountId])
82 | }
83 |
84 | model MySQL {
85 | id String @id @default(cuid())
86 | userId String
87 | user User @relation(fields: [userId], references: [id])
88 | mysql_username String // TODO: store securely
89 | mysql_password String // TODO: store securely
90 | Account Account? @relation(fields: [accountId], references: [id])
91 | accountId String?
92 |
93 | @@index([userId])
94 | @@index([accountId])
95 | }
96 |
--------------------------------------------------------------------------------
/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @see https://create.t3.gg/en/usage/prisma
3 | *
4 | * */
5 |
6 | import { prisma } from "../src/server/db";
7 |
8 | async function main() {
9 | const id = "cl9ebqhxk00003b600tymydho";
10 | await prisma.example.upsert({
11 | where: {
12 | id,
13 | },
14 | create: {
15 | id,
16 | },
17 | update: {},
18 | });
19 | }
20 |
21 | main()
22 | .then(async () => {
23 | await prisma.$disconnect();
24 | })
25 | .catch(async (e) => {
26 | console.error(e);
27 | await prisma.$disconnect();
28 | process.exit(1);
29 | });
30 |
--------------------------------------------------------------------------------
/public/assets/Demo_connectDB.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/Demo_connectDB.gif
--------------------------------------------------------------------------------
/public/assets/Demo_queryInput.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/Demo_queryInput.gif
--------------------------------------------------------------------------------
/public/assets/Zoom Background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/Zoom Background.jpg
--------------------------------------------------------------------------------
/public/assets/backgroundContainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/backgroundContainer.png
--------------------------------------------------------------------------------
/public/assets/github-mark-white-.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/github-mark-white-.png
--------------------------------------------------------------------------------
/public/assets/linkedin-icon-update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/linkedin-icon-update.png
--------------------------------------------------------------------------------
/public/assets/logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-128.png
--------------------------------------------------------------------------------
/public/assets/logo-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-32.png
--------------------------------------------------------------------------------
/public/assets/logo-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-64.png
--------------------------------------------------------------------------------
/public/assets/logo-full-background-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-full-background-color.png
--------------------------------------------------------------------------------
/public/assets/logo-full-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-full-bg.png
--------------------------------------------------------------------------------
/public/assets/logo-full-no-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/assets/logo-full-no-bg.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/public/favicon.ico
--------------------------------------------------------------------------------
/src/components/AuthShowcase.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { signIn, signOut, useSession } from 'next-auth/react';
3 | import Button from './Button';
4 |
5 | const AuthShowcase: React.FC = () => {
6 | const { data: sessionData } = useSession();
7 |
8 | const profilePicStyle: React.CSSProperties = {
9 | borderRadius: '50%',
10 | };
11 |
12 | return (
13 |
14 |
15 | {sessionData && (
16 |
17 | {/* eslint-disable-next-line @next/next/no-img-element */}
18 |
24 |
{sessionData.user?.name}
25 | {/* eslint-disable-next-line @next/next/no-img-element */}
26 |
27 | )}
28 |
29 | {/* {sessionData && (
30 |
31 |
32 |
33 | {sessionData ? 'Continue to app' : ''}
34 |
35 |
36 |
37 | )} */}
38 |
void signOut()
43 | () => window.location.replace('/homepage')
44 | : () =>
45 | void signIn(sessionData, {
46 | callbackUrl: `${window.location.origin}/homepage`,
47 | })
48 | }
49 | >
50 | {sessionData ? 'Continue' : 'Sign in'}
51 |
52 |
53 | );
54 | };
55 |
56 | export default AuthShowcase;
57 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | interface ButtonProps {
4 | btnStyle: (props: T) => T;
5 | }
6 |
7 | const Button: React.FC = (props: ButtonProps, children) => {
8 | const { btnStyle } = props;
9 |
10 | return (
11 | <>
12 | {children}
13 | >
14 | );
15 | };
16 |
17 | export default Button;
18 |
--------------------------------------------------------------------------------
/src/components/DBConnect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { DBConnectProps } from '~/types/types';
3 | import { BsDatabaseAdd } from 'react-icons/bs';
4 |
5 | const DBConnect: React.FC = ({
6 | openModal,
7 | connection,
8 | formData,
9 | setFormData,
10 | disconnectDB,
11 | }) => {
12 | const handleConnect = () => {
13 | setFormData({
14 | graf_name: '',
15 | graf_pass: '',
16 | graf_port: '',
17 | db_name: '',
18 | db_url: '',
19 | db_username: '',
20 | db_server: '',
21 | db_password: '',
22 | });
23 | openModal(true);
24 | };
25 |
26 | const handleClick = async () => {
27 | await disconnectDB();
28 | };
29 |
30 | return (
31 |
32 | {!connection ? (
33 | <>
34 |
38 |
39 |
40 |
41 |
42 | Connect to Database
43 |
44 |
45 | >
46 | ) : (
47 | <>
48 |
49 |
50 | Active Connection
51 |
52 |
53 |
54 | DB Name:{' '}
55 | {formData.db_name}
56 |
57 |
58 |
59 | DB Server:{' '}
60 | {formData.db_server}
61 |
62 |
63 |
67 | Disconnect
68 |
69 |
70 |
71 | >
72 | )}
73 |
74 | );
75 | };
76 |
77 | export default DBConnect;
78 |
--------------------------------------------------------------------------------
/src/components/GraphCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { GraphCardProps } from '~/types/types';
3 |
4 | const GraphCard: React.FC = ({ src, key }) => {
5 | return (
6 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default GraphCard;
16 |
--------------------------------------------------------------------------------
/src/components/HamburgerMenu.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { GiHamburgerMenu } from 'react-icons/gi';
3 | import { signOut, signIn, useSession } from 'next-auth/react';
4 | import Link from 'next/link';
5 |
6 | const HamburgerMenu = () => {
7 | const { data: sessionData } = useSession();
8 | const [isOpen, setIsOpen] = useState(false);
9 |
10 | const toggleMenu = () => {
11 | setIsOpen(!isOpen);
12 | };
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | {isOpen && (
20 |
21 |
22 |
23 | Home
24 |
25 |
26 | About
27 |
28 |
29 | FAQ
30 |
31 |
32 | Contact
33 |
34 |
35 |
36 | Docs
37 |
38 |
39 | {
44 | void signOut({ callbackUrl: window.location.origin });
45 | }
46 | : () => {
47 | void signIn();
48 | }
49 | }
50 | >
51 | {sessionData ? 'Logout' : 'Sign in'}
52 |
53 |
54 |
55 | )}
56 |
57 | );
58 | };
59 |
60 | export default HamburgerMenu;
61 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 | import HamburgerMenu from '../components/HamburgerMenu';
4 | import Link from 'next/link';
5 |
6 | const Header: React.FC = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 | Query IQ
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Header;
36 |
--------------------------------------------------------------------------------
/src/components/InputQuery.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState } from 'react';
3 | import LoadingBar from './LoadingBar';
4 | import Popup from './Popup';
5 | import type {
6 | InputQueryProps,
7 | GrafanaUserObject,
8 | QueryLogItemObject,
9 | dbUid,
10 | } from '~/types/types';
11 | import { useMutation } from 'react-query';
12 | import { BsKeyboard, BsArrowRightCircleFill } from 'react-icons/bs';
13 |
14 | const InputQuery: React.FC = ({
15 | setQueryLog,
16 | setQuery,
17 | query,
18 | setActiveQuery,
19 | setDashboardState,
20 | grafanaUser,
21 | dbUid,
22 | connection,
23 | }) => {
24 | //useState for loading bar
25 | const [loadingProgress, setLoadingProgress] = useState(0);
26 | const [isLoading, setIsLoading] = useState(false);
27 |
28 | const asyncLoadingSim = (): Promise => {
29 | setIsLoading(true);
30 | setLoadingProgress(0);
31 | return new Promise((resolve) => {
32 | setTimeout(() => {
33 | setLoadingProgress(100);
34 | setTimeout(() => {
35 | setIsLoading(false);
36 | resolve();
37 | }, 900);
38 | }, 100);
39 | });
40 | };
41 |
42 | //KT's code for fetching POST for the input query dashboard to Grafana
43 | //use mutation from react query to fetch a post request to send api to create dashboard for input query
44 |
45 | const mutationQuery = useMutation(
46 | async ({
47 | query,
48 | dbUid,
49 | grafanaUser,
50 | }: {
51 | query: string;
52 | dbUid: dbUid;
53 | grafanaUser: GrafanaUserObject;
54 | }) => {
55 | const apiUrl = 'http://localhost:3001/api/query';
56 | //deconstruct query for the request response
57 | const response = await fetch(apiUrl, {
58 | method: 'POST',
59 | headers: {
60 | 'Content-Type': 'application/json',
61 | },
62 | body: JSON.stringify({
63 | query: query,
64 | GrafanaCredentials: {
65 | graf_name: grafanaUser.graf_name,
66 | graf_port: grafanaUser.graf_port,
67 | graf_pass: grafanaUser.graf_pass,
68 | },
69 | datasourceUID: dbUid.datasourceUid,
70 | }),
71 | });
72 | // If response is less than 200 or greater than 300
73 | // Basically, if response is NOT 200-299
74 | if (response.status <= 199 && response.status >= 300) {
75 | throw new Error('Failed to connect'); // Handle error
76 | }
77 | return response.json();
78 | }
79 | );
80 |
81 | const handleGoClick = async (
82 | e: React.FormEvent
83 | ): Promise => {
84 | e.preventDefault();
85 | try {
86 | const response = (await mutationQuery.mutateAsync({
87 | query,
88 | dbUid,
89 | grafanaUser,
90 | })) as void | {
91 | slug: string;
92 | uid: string;
93 | status: number;
94 | iFrames: string[];
95 | };
96 | await asyncLoadingSim();
97 | const { iFrames, uid } = response;
98 | const newQuery: QueryLogItemObject = {
99 | query: query,
100 | data: iFrames,
101 | name: '',
102 | dashboardUID: uid,
103 | };
104 | setQueryLog((prevQueryLog) => [...prevQueryLog, newQuery]);
105 | setQuery('');
106 | setActiveQuery(newQuery);
107 | setDashboardState('query');
108 | } catch (error) {
109 | console.error(error);
110 | }
111 | };
112 | // TO DO: want to move this conditional to the return statement and plug in our loading bar component
113 | //if post request is still loading
114 | if (mutationQuery.isLoading) {
115 | return ;
116 | }
117 |
118 | // //if post request fails to fetch
119 | if (mutationQuery.error) {
120 | return ;
121 | }
122 |
123 | return (
124 | <>
125 |
157 | >
158 | );
159 | };
160 |
161 | export default InputQuery;
162 |
--------------------------------------------------------------------------------
/src/components/LoadingBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { LoadingBarProps } from '~/types/types';
3 |
4 | const LoadingBar: React.FC = ({ loadingProgress }) => {
5 | return (
6 |
7 |
11 | {' '}
12 | {/* {loadingProgress}% */}
13 | {'Loading...'}
14 |
15 |
16 | );
17 | };
18 |
19 | export default LoadingBar;
20 |
--------------------------------------------------------------------------------
/src/components/Popup.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type PopupProps = {
4 | text: string;
5 | };
6 |
7 | const Popup: React.FC = ({ text }) => {
8 | const handleClick = () => {
9 | location.reload();
10 | };
11 |
12 | return (
13 | // Background overlay
14 |
15 | {/* Popup Container */}
16 |
17 | {/* Popup Title Text */}
18 |
{text}
19 |
23 | {/* Button Text */}
24 | Cancel
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default Popup;
32 |
--------------------------------------------------------------------------------
/src/components/modal/DBCredentials.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { DBCredentialsProps } from '~/types/types';
3 | import ModalFormInput from './ModalFormInput';
4 |
5 | const DBCredentials: React.FC = ({
6 | formData,
7 | setFormData,
8 | handleConnect,
9 | isFormValid,
10 | handleCancel,
11 | }) => {
12 | return (
13 |
14 | Database Name
15 | setFormData({ ...formData, db_name: e.target.value })}
20 | />
21 | Database URL
22 | setFormData({ ...formData, db_url: e.target.value })}
27 | />
28 | Database Username
29 |
34 | setFormData({ ...formData, db_username: e.target.value })
35 | }
36 | />
37 | Database Server
38 |
43 | setFormData({ ...formData, db_server: e.target.value })
44 | }
45 | />
46 | Database Password
47 |
52 | setFormData({ ...formData, db_password: e.target.value })
53 | }
54 | />
55 |
56 |
61 | Connect
62 |
63 |
67 | Cancel
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default DBCredentials;
75 |
--------------------------------------------------------------------------------
/src/components/modal/DBModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState } from 'react';
3 | import DBSelection from './DBSelection';
4 | import DBCredentials from './DBCredentials';
5 | import GrafanaCredentials from './GrafanaCredentials';
6 | import type { DBModalProps } from '~/types/types';
7 |
8 | const DBModal: React.FC = ({
9 | openModal,
10 | setFormData,
11 | formData,
12 | handleConnect,
13 | isFormValid,
14 | setGrafanaUser,
15 | grafanaUser,
16 | }) => {
17 | // used to cycle between modal states, selecting a database and inputting credentials
18 | const [dbSelection, setdbSelection] = useState(0);
19 |
20 | const handleClick = () => {
21 | setdbSelection((prevState) => prevState + 1);
22 | };
23 |
24 | const handleCancel = () => {
25 | setdbSelection(0);
26 | openModal(false);
27 | };
28 |
29 | return (
30 |
31 |
32 |
Connect to a Database
33 | {dbSelection === 0 ? (
34 |
35 | ) : dbSelection === 1 ? (
36 |
44 | ) : dbSelection === 2 ? (
45 |
52 | ) : null}
53 |
54 |
55 | );
56 | };
57 |
58 | export default DBModal;
59 |
--------------------------------------------------------------------------------
/src/components/modal/DBSelection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { DBSelectionProps } from '~/types/types';
3 |
4 | const DBSelection: React.FC = ({
5 | handleCancel,
6 | handleClick,
7 | }) => {
8 | return (
9 |
10 |
14 | PostgreSQL
15 |
16 |
20 | Cancel
21 |
22 |
23 | );
24 | };
25 |
26 | export default DBSelection;
27 |
--------------------------------------------------------------------------------
/src/components/modal/GrafanaCredentials.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { GrafanaCredentialsProps } from '~/types/types';
3 | import ModalFormInput from './ModalFormInput';
4 |
5 | const GrafanaCredentials: React.FC = ({
6 | handleCancel,
7 | handleClick,
8 | formData,
9 | setFormData,
10 | setGrafanaUser,
11 | grafanaUser,
12 | }) => {
13 | return (
14 |
15 | Grafana Username
16 | {
21 | const { value } = e.target;
22 | setFormData({ ...formData, graf_name: value });
23 | setGrafanaUser({ ...grafanaUser, graf_name: value });
24 | }}
25 | />
26 | {/* */}
27 | Grafana Password
28 | {
33 | const { value } = e.target;
34 | setFormData({ ...formData, graf_pass: value });
35 | setGrafanaUser({ ...grafanaUser, graf_pass: value });
36 | }}
37 | />
38 | Grafana Port
39 | {
44 | const { value } = e.target;
45 | setFormData({ ...formData, graf_port: value });
46 | setGrafanaUser({ ...grafanaUser, graf_port: value });
47 | }}
48 | />
49 |
50 |
57 | Next
58 |
59 |
63 | Cancel
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default GrafanaCredentials;
71 |
--------------------------------------------------------------------------------
/src/components/modal/ModalFormInput.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import type { ModalFormInputProps } from '~/types/types';
3 |
4 | const ModalFormInput: React.FC = ({
5 | placeholder,
6 | type,
7 | value,
8 | onChange,
9 | }) => {
10 | return (
11 |
18 | );
19 | };
20 |
21 | export default ModalFormInput;
22 |
--------------------------------------------------------------------------------
/src/components/queryLog/EditButton.tsx:
--------------------------------------------------------------------------------
1 | // tried modularizing editbutton
2 |
3 | // import React from "react";
4 |
5 | // interface EditButtonProps {
6 | // setEditMode: React.Dispatch>;
7 | // isHovered: boolean;
8 | // handleEditHover: (bool: boolean) => void;
9 | // children: React.ReactNode;
10 | // }
11 |
12 | // const EditButton: React.FC = ({
13 | // setEditMode,
14 | // handleEditHover,
15 | // isHovered,
16 | // children,
17 | // }) => {
18 | // return (
19 | // setEditMode(false)}
21 | // onMouseEnter={() => handleEditHover(true)}
22 | // onMouseLeave={() => handleEditHover(false)}
23 | // className={isHovered ? "edit-icon" : "edit-icon hidden"}
24 | // >
25 | // {children} {/* Render the children */}
26 | //
27 | // );
28 | // };
29 |
30 | // export default EditButton;
31 |
--------------------------------------------------------------------------------
/src/components/queryLog/QueryLog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import { useState } from "react";
3 | import QueryLogItem from './QueryLogItem';
4 | import type { QueryLogProps } from '~/types/types';
5 | import { BsJournalText } from 'react-icons/bs';
6 |
7 | const QueryLog: React.FC = ({
8 | queryLog,
9 | editQueryLabel,
10 | deleteQuery,
11 | setActiveQuery,
12 | activeQuery,
13 | setDashboardState,
14 | }) => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Query Log
22 |
23 |
24 |
25 | {queryLog.map((_query, index) => {
26 | const queryLogObject = queryLog[index];
27 | if (queryLogObject) {
28 | return (
29 |
39 | );
40 | }
41 | return null;
42 | })}
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default QueryLog;
50 |
--------------------------------------------------------------------------------
/src/components/queryLog/QueryLogItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState } from 'react';
3 | import {
4 | AiFillEdit,
5 | AiOutlineEdit,
6 | AiFillCheckSquare,
7 | AiOutlineCheckSquare,
8 | AiFillDelete,
9 | AiOutlineDelete,
10 | } from 'react-icons/ai';
11 | import type { QueryLogItemProps } from '~/types/types';
12 |
13 | const QueryLogItem: React.FC = ({
14 | index,
15 | editQueryLabel,
16 | deleteQuery,
17 | queryLogObject,
18 | setActiveQuery,
19 | activeQuery,
20 | setDashboardState,
21 | }) => {
22 | const [editMode, setEditMode] = useState(false);
23 | const [label, setLabel] = useState('');
24 | // const [active, setActive] = useState(false);
25 | const [isEditHovered, setIsEditHovered] = useState(false);
26 | const [isDeleteHovered, setIsDeleteHovered] = useState(false);
27 |
28 | const handleEditHover = (bool: boolean) => {
29 | setIsEditHovered(bool);
30 | };
31 |
32 | const handleDeleteHover = (bool: boolean) => {
33 | setIsDeleteHovered(bool);
34 | };
35 |
36 | const handleFormSubmit = (e: React.FormEvent) => {
37 | e.preventDefault();
38 | editQueryLabel(index, label);
39 | setEditMode(false);
40 | };
41 |
42 | const handleEditClick = (e: React.MouseEvent) => {
43 | e.stopPropagation(); // Prevent event propagation to parent div
44 | setEditMode(true);
45 | };
46 |
47 | const handleDeleteClick = async (e: React.MouseEvent) => {
48 | e.stopPropagation(); // Prevent event propagation to parent div
49 | await deleteQuery(index);
50 | };
51 |
52 | const handleClick = () => {
53 | setDashboardState('query');
54 | setActiveQuery(queryLogObject);
55 | };
56 |
57 | return (
58 |
59 | {!editMode ? (
60 |
69 | {queryLogObject.name ? queryLogObject.name : `Query ${index + 1}`}{' '}
70 |
71 |
handleEditHover(true)}
73 | onMouseLeave={() => handleEditHover(false)}
74 | onClick={handleEditClick}
75 | >
76 |
80 |
84 |
85 |
86 |
handleDeleteHover(true)}
88 | onMouseLeave={() => handleDeleteHover(false)}
89 | onClick={handleDeleteClick}
90 | >
91 |
95 |
101 |
102 |
103 |
104 |
105 | ) : (
106 |
107 |
111 | setLabel(e.target.value)}
116 | >
117 |
118 |
handleEditHover(true)}
121 | onMouseLeave={() => handleEditHover(false)}
122 | className={isEditHovered ? 'edit-icon' : 'edit-icon hidden'}
123 | >
124 |
125 |
126 |
handleEditHover(true)}
129 | onMouseLeave={() => handleEditHover(false)}
130 | className={!isEditHovered ? 'edit-icon' : 'edit-icon hidden'}
131 | >
132 |
133 |
134 |
135 |
136 |
137 | )}
138 |
139 | );
140 | };
141 |
142 | export default QueryLogItem;
143 |
--------------------------------------------------------------------------------
/src/containers/DashboardContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GraphCard from '~/components/GraphCard';
3 | import type { DashboardContainerProps } from '~/types/types';
4 | import { GiHealthNormal } from 'react-icons/gi';
5 | import { AiOutlineLineChart } from 'react-icons/ai';
6 |
7 | const DashboardContainer: React.FC = ({
8 | activeQuery,
9 | dashboardState,
10 | setDashboardState,
11 | databaseGraphs,
12 | connection,
13 | }) => {
14 | return (
15 |
16 |
17 |
18 |
setDashboardState('database')}
23 | >
24 |
25 |
26 |
27 | Database Health Metrics
28 |
29 |
setDashboardState('query')}
34 | >
35 |
36 |
37 |
38 | Active Query
39 |
40 |
41 | {dashboardState === 'database' ? (
42 |
43 | {!connection ? (
44 | <>>
45 | ) : (
46 | <>
47 | {databaseGraphs.map((src, index) => {
48 | return ;
49 | })}
50 | >
51 | )}
52 |
53 | ) : dashboardState === 'query' ? (
54 |
55 |
56 | {activeQuery.query}
57 |
58 |
59 | {activeQuery.data.map((src, index) => {
60 | return ;
61 | })}
62 |
63 |
64 | ) : null}
65 |
66 |
67 | );
68 | };
69 |
70 | export default DashboardContainer;
71 |
--------------------------------------------------------------------------------
/src/containers/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState, useEffect } from 'react';
3 | import { useMutation } from 'react-query';
4 | import QueryContainer from './QueryContainer';
5 | import SideBarContainer from './SideBarContainer';
6 | import DBModal from '~/components/modal/DBModal';
7 | import Popup from '~/components/Popup';
8 | import type {
9 | QueryLogItemObject,
10 | FormData,
11 | GrafanaUserObject,
12 | } from '~/types/types';
13 |
14 | const MainContainer: React.FC = ({}) => {
15 | const [isModalOpen, setIsModalOpen] = useState(false);
16 | const [query, setQuery] = useState(''); // inputQuery state
17 | const [queryLog, setQueryLog] = useState([]);
18 | const [connection, setConnection] = useState(false);
19 |
20 | const [grafanaUser, setGrafanaUser] = useState({
21 | graf_name: '',
22 | graf_pass: '',
23 | graf_port: '',
24 | });
25 | const [isFormValid, setIsFormValid] = useState(false);
26 | const [formData, setFormData] = useState({
27 | graf_name: '',
28 | graf_pass: '',
29 | graf_port: '',
30 | db_name: '',
31 | db_url: '',
32 | db_username: '',
33 | db_server: '',
34 | db_password: '',
35 | });
36 | const [dbUid, setdbUid] = useState({ datasourceUid: '', dashboardUid: '' });
37 |
38 | const [dashboardState, setDashboardState] = useState('database'); // alt state is 'query'
39 | const [databaseGraphs, setDatabaseGraphs] = useState([]);
40 |
41 | const [activeQuery, setActiveQuery] = useState({
42 | query: '',
43 | data: [],
44 | name: '',
45 | dashboardUID: '',
46 | });
47 |
48 | //checking form validation on input changes for credentials
49 | useEffect(() => {
50 | const {
51 | graf_name,
52 | graf_pass,
53 | graf_port,
54 | db_name,
55 | db_url,
56 | db_username,
57 | db_server,
58 | db_password,
59 | } = formData;
60 | let isValid = false;
61 | if (
62 | graf_name &&
63 | graf_pass &&
64 | graf_port &&
65 | db_name &&
66 | db_url &&
67 | db_username &&
68 | db_server &&
69 | db_password
70 | ) {
71 | isValid = true;
72 | }
73 | setIsFormValid(isValid);
74 | }, [formData]);
75 |
76 | //when form is submitted, the function passed to useMutation is executed. It will receive the formData as an argument, which contains the data entered in the form fields. useMutation is used for making POST,PUT,DELETE
77 | const mutation = useMutation(async (formData: FormData) => {
78 | const apiUrl = 'http://localhost:3001/api/connect';
79 | const {
80 | graf_name,
81 | graf_pass,
82 | graf_port,
83 | db_name,
84 | db_url,
85 | db_username,
86 | db_server,
87 | db_password,
88 | } = formData;
89 |
90 | const response = await fetch(apiUrl, {
91 | method: 'POST',
92 | headers: {
93 | 'Content-Type': 'application/json',
94 | Accept: 'application/json',
95 | },
96 | body: JSON.stringify({
97 | graf_name,
98 | graf_pass,
99 | graf_port,
100 | db_name,
101 | db_url,
102 | db_username,
103 | db_server,
104 | db_password,
105 | }),
106 | });
107 | if (!response.ok) {
108 | throw new Error('Failed to connect'); // Handle error
109 | }
110 |
111 | return response.json();
112 | });
113 |
114 | // will only fire if isFormValid === true
115 | const handleConnect = async () => {
116 | try {
117 | // mutation is an object returned by the useMutation hook and mutateAsync is a method provided by mutation object
118 | // await mutation.mutateAsync waits for the mutation operation to complete before moving to the next line
119 | const response = (await mutation.mutateAsync(formData)) as void | {
120 | slug: string;
121 | uid: string;
122 | status: number;
123 | iFrames: string[];
124 | };
125 | // if response is NOT 200-299
126 | if (response.status <= 199 && response.status >= 300) {
127 | throw new Error('Failed to connect');
128 | }
129 | const { iFrames, datasourceuid, dashboarduid } = response;
130 | setdbUid({ datasourceUid: datasourceuid, dashboardUid: dashboarduid });
131 | setDatabaseGraphs(iFrames);
132 | setDashboardState('database');
133 | setConnection(true);
134 | setIsModalOpen(false);
135 | } catch (error) {
136 | console.error(error);
137 | }
138 | };
139 |
140 | // finds querylog object in array and updates the name property
141 | const editQueryLabel = (index: number, label: string): void => {
142 | setQueryLog((prevQueryLog) => {
143 | if (prevQueryLog.length > index) {
144 | const updatedQueryLog = [...prevQueryLog];
145 | updatedQueryLog[index].name = label; // functionality works but this linter is not being nice!
146 | return updatedQueryLog;
147 | }
148 | return prevQueryLog;
149 | });
150 | };
151 |
152 | const deleteQuery = async (index: number): Promise => {
153 | const queryToDelete = queryLog[index];
154 | const isDeletingActiveQuery = queryToDelete === activeQuery;
155 | try {
156 | // make async call to backend to delete query specific dashboard
157 | const url = 'http://localhost:3001/api/delete';
158 | const response = await fetch(url, {
159 | method: 'DELETE',
160 | headers: {
161 | 'Content-Type': 'application/json',
162 | Accept: 'application/json',
163 | },
164 | body: JSON.stringify({
165 | dashboardUID: queryToDelete.dashboardUID,
166 | datasourceUID: dbUid,
167 | GrafanaCredentials: {
168 | graf_port: grafanaUser.graf_port,
169 | graf_name: grafanaUser.graf_name,
170 | graf_pass: grafanaUser.graf_pass,
171 | },
172 | }),
173 | });
174 | const data = await response.json();
175 | if (data.status <= 199 && response.status >= 300) {
176 | throw new Error('Failed to connect');
177 | }
178 | setQueryLog((prevQueryLog) => {
179 | if (prevQueryLog.length > index) {
180 | const updatedQueryLog = [...prevQueryLog];
181 | updatedQueryLog.splice(index, 1);
182 | return updatedQueryLog;
183 | }
184 | });
185 | if (isDeletingActiveQuery) {
186 | setActiveQuery({
187 | query: '',
188 | data: [],
189 | name: '',
190 | dashboardUID: '',
191 | });
192 | }
193 | } catch (err) {
194 | console.log(err);
195 | }
196 | };
197 |
198 | const disconnectDB = async (): Promise => {
199 | try {
200 | // make async call to backend to delete query specific dashboard
201 | const url = 'http://localhost:3001/api/disconnect';
202 | const response = await fetch(url, {
203 | method: 'DELETE',
204 | headers: {
205 | 'Content-Type': 'application/json',
206 | Accept: 'application/json',
207 | },
208 | body: JSON.stringify({
209 | dashboardUID: dbUid.dashboardUid,
210 | datasourceUID: dbUid.datasourceUid,
211 | GrafanaCredentials: {
212 | graf_port: grafanaUser.graf_port,
213 | graf_name: grafanaUser.graf_name,
214 | graf_pass: grafanaUser.graf_pass,
215 | },
216 | }),
217 | });
218 | const data = await response.json();
219 | if (data.status <= 199 && response.status >= 300) {
220 | throw new Error('Failed to connect');
221 | }
222 | setConnection(false);
223 | setDatabaseGraphs([]);
224 | setdbUid({ datasourceUid: '', dashboardUid: '' });
225 | setGrafanaUser({
226 | graf_name: '',
227 | graf_pass: '',
228 | graf_port: '',
229 | });
230 | } catch (err) {
231 | console.log(err);
232 | }
233 | };
234 |
235 | // TO DO: want to move this conditional to the return statement and plug in our loading bar component
236 | //if post request is still loading
237 | if (mutation.isLoading) {
238 | return ;
239 | }
240 |
241 | // //if post request fails to fetch
242 | if (mutation.error) {
243 | return ;
244 | }
245 |
246 | return (
247 |
248 | {/* {!mutation.isLoading ? <>> :
}
249 | {!mutation.error ? <>> :
} */}
250 | {!isModalOpen ? (
251 | <>>
252 | ) : (
253 | <>
254 |
263 | >
264 | )}
265 |
279 |
292 |
293 | );
294 | };
295 |
296 | export default MainContainer;
297 |
--------------------------------------------------------------------------------
/src/containers/QueryContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DashboardContainer from './DashboardContainer';
3 | import InputQuery from '../components/InputQuery';
4 | import type { QueryContainerProps } from '~/types/types';
5 |
6 | //Container is for input query and render the graphs on Dashboard Container if input query is processed successfully
7 |
8 | const QueryContainer: React.FC = ({
9 | setQueryLog,
10 | setQuery,
11 | query,
12 | setActiveQuery,
13 | activeQuery,
14 | dashboardState,
15 | setDashboardState,
16 | databaseGraphs,
17 | connection,
18 | grafanaUser,
19 | dbUid,
20 | }) => {
21 | return (
22 |
48 | );
49 | };
50 |
51 | export default QueryContainer;
52 |
--------------------------------------------------------------------------------
/src/containers/SideBarContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DBConnect from '~/components/DBConnect';
3 | import QueryLog from '~/components/queryLog/QueryLog';
4 | import type { SideBarContainerProps } from '~/types/types';
5 |
6 | const SideBarContainer: React.FC = ({
7 | openModal,
8 | connection,
9 | formData,
10 | setFormData,
11 | queryLog,
12 | editQueryLabel,
13 | deleteQuery,
14 | setActiveQuery,
15 | activeQuery,
16 | setDashboardState,
17 | disconnectDB,
18 | }) => {
19 | return (
20 |
21 |
28 |
36 |
37 | );
38 | };
39 |
40 |
41 |
42 | export default SideBarContainer;
43 |
--------------------------------------------------------------------------------
/src/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from '@t3-oss/env-nextjs';
2 | import { z } from 'zod';
3 |
4 | export const env = createEnv({
5 | /**
6 | * Specify your server-side environment variables schema here. This way you can ensure the app
7 | * isn't built with invalid env vars.
8 | */
9 | server: {
10 | PORT_EXPRESS: z.string(),
11 | DATABASE_URL_PRISMA: z.string().url(),
12 | DATABASE_URL_NODE: z.string().url(),
13 | NODE_ENV: z.enum(['development', 'test', 'production']),
14 | NEXTAUTH_SECRET:
15 | process.env.NODE_ENV === 'production'
16 | ? z.string().min(1)
17 | : z.string().min(1).optional(),
18 | NEXTAUTH_URL: z.preprocess(
19 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
20 | // Since NextAuth.js automatically uses the VERCEL_URL if present.
21 | (str) => process.env.VERCEL_URL ?? str,
22 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL
23 | process.env.VERCEL ? z.string().min(1) : z.string().url()
24 | ),
25 | // Add `.min(1) on ID and SECRET if you want to make sure they're not empty
26 | GITHUB_ID: z.string(),
27 | GITHUB_SECRET: z.string(),
28 | },
29 |
30 | /**
31 | * Specify your client-side environment variables schema here. This way you can ensure the app
32 | * isn't built with invalid env vars. To expose them to the client, prefix them with
33 | * `NEXT_PUBLIC_`.
34 | */
35 | client: {
36 | // NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
37 | },
38 |
39 | /**
40 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
41 | * middlewares) or client-side so we need to destruct manually.
42 | */
43 | runtimeEnv: {
44 | PORT_EXPRESS: process.env.PORT_EXPRESS,
45 | DATABASE_URL_PRISMA: process.env.DATABASE_URL_PRISMA,
46 | DATABASE_URL_NODE: process.env.DATABASE_URL_NODE,
47 | NODE_ENV: process.env.NODE_ENV,
48 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
49 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
50 | GITHUB_ID: process.env.GITHUB_ID,
51 | GITHUB_SECRET: process.env.GITHUB_SECRET,
52 | },
53 | /**
54 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
55 | * This is especially useful for Docker builds.
56 | */
57 | skipValidation: !!process.env.SKIP_ENV_VALIDATION,
58 | });
59 |
--------------------------------------------------------------------------------
/src/pages/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 |
3 | /**
4 | * @see https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes
5 | */
6 | export default function Page() {
7 | const router = useRouter()
8 | return Post: {router.query.id}
9 | }
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { type Session } from 'next-auth';
2 | import { SessionProvider } from 'next-auth/react';
3 | import { type AppType } from 'next/app';
4 | import { QueryClient, QueryClientProvider } from 'react-query';
5 | import '~/styles/globals.css';
6 |
7 | const queryClient = new QueryClient();
8 |
9 | const MyApp: AppType<{ session: Session | null }> = ({
10 | Component,
11 | pageProps: { session, ...pageProps },
12 | }) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default MyApp;
23 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../components/Header';
3 | import Head from 'next/head';
4 |
5 | const About: React.FC = () => {
6 | return (
7 | <>
8 |
9 | About Page
10 |
11 |
12 |
13 |
14 |
15 |
16 |
About Us
17 |
18 | We are proud to present QueryIQ, a powerful tool that empowers developers to take control of their PostgreSQL databases.
19 | Whether you are an experienced developer looking to optimize performance or a novice seeking guidance,
20 | QueryIQ is here to simplify the process and enhance your development experience.
21 |
22 |
23 | The main goal of Query IQ is to provide a one-stop-shop for developers who seek to fine-tune their application’s interactions with a SQL database.
24 | By connecting their database, QueryIQ enables developers to gain valuable insights on their database from a holistic standpoint,
25 | and to receive performance data through live query execution directly to their database.
26 |
27 |
28 |
29 | >
30 | );
31 | };
32 |
33 | export default About;
34 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import { authOptions } from '~/server/auth';
3 |
4 | export default NextAuth(authOptions);
5 |
--------------------------------------------------------------------------------
/src/pages/api/restricted.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import { authOptions } from "~/server/auth"
3 | import NextAuth from "./auth/[...nextauth]"
4 |
5 |
6 | export default async (req: Request, res: Response): Promise => {
7 | const session = await getServerSession(req, res, authOptions)
8 | if (session) {
9 | res.send({
10 | content:
11 | "This is protected content. You can access this content because you are signed in.",
12 | })
13 | } else {
14 | res.send({
15 | error: "You must be signed in to view the content on this page.",
16 | })
17 | }
18 | }
--------------------------------------------------------------------------------
/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | import { createNextApiHandler } from "@trpc/server/adapters/next";
2 | import { env } from "~/env.mjs";
3 | import { appRouter } from "~/server/api/root";
4 | import { createTRPCContext } from "~/server/api/trpc";
5 |
6 | // export API handler
7 | export default createNextApiHandler({
8 | router: appRouter,
9 | createContext: createTRPCContext,
10 | onError:
11 | env.NODE_ENV === "development"
12 | ? ({ path, error }) => {
13 | console.error(
14 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`,
15 | );
16 | }
17 | : undefined,
18 | });
19 |
--------------------------------------------------------------------------------
/src/pages/contact.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../components/Header';
3 | import Head from 'next/head';
4 | import Image from 'next/image';
5 |
6 | const Contact = () => {
7 | const teamMembers = [
8 | {
9 | image: 'https://github.com/KAlanBeck.png',
10 | name: 'Alan Beck',
11 | linkedin: 'https://www.linkedin.com/in/k-alan-beck/',
12 | github: 'https://github.com/KAlanBeck/',
13 | },
14 | {
15 | image: 'https://github.com/connoro7.png',
16 | name: 'Connor Dillon',
17 | linkedin: 'https://www.linkedin.com/in/connor-dillon/',
18 | github: 'https://github.com/connoro7/',
19 | },
20 | {
21 | image: 'https://github.com/deanbiscocho.png',
22 | name: 'Dean Biscocho',
23 | linkedin: 'https://www.linkedin.com/in/deanbiscocho/',
24 | github: 'https://github.com/deanbiscocho/',
25 | },
26 | {
27 | image: 'https://github.com/jojecameron.png',
28 | name: 'Johanna Cameron',
29 | linkedin: 'https://www.linkedin.com/in/johanna-cameron/',
30 | github: 'https://github.com/jojecameron/',
31 | },
32 | {
33 | image: 'https://github.com/khailetran.png',
34 | name: 'Khaile Tran',
35 | linkedin: 'https://www.linkedin.com/in/khailetran/',
36 | github: 'https://github.com/khailetran/',
37 | },
38 | // Add more team members as needed
39 | ];
40 | return (
41 | <>
42 |
43 | Contact
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Meet Our Team
51 |
52 | {teamMembers.map((member, index) => (
53 |
113 | ))}
114 |
115 |
116 |
117 | >
118 | );
119 | };
120 |
121 | export default Contact;
122 |
--------------------------------------------------------------------------------
/src/pages/faq.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from '../components/Header';
3 | import Head from 'next/head';
4 |
5 | const FAQ = () => {
6 | return (
7 | <>
8 |
9 | FAQ
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | FAQ
22 |
23 |
24 | Here are some of the frequently asked questions
25 |
26 |
27 |
28 |
29 |
30 | Q.
31 |
32 |
33 | A.
34 |
35 |
36 |
37 |
38 |
39 | What are performance metrics for SQL database queries?
40 |
41 |
42 |
43 |
44 | Performance metrics for SQL database queries are
45 | measurements used to assess the efficiency and
46 | effectiveness of query execution. They include metrics
47 | such as query execution time, query throughput, CPU usage,
48 | memory usage, disk I/O, and more.
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Q.
58 |
59 |
60 | A.
61 |
62 |
63 |
64 |
65 |
66 | Why do performance metrics for database queries matter?
67 |
68 |
69 |
70 |
71 | Performance metrics for database queries matter because
72 | they provide insights into system efficiency, user
73 | experience, scalability, cost optimization,
74 | troubleshooting, optimization opportunities, SLA
75 | compliance, and enable continuous improvement. Monitoring
76 | and analyzing these metrics help maintain a
77 | well-performing database system and ensure that it meets
78 | the needs of users and applications.
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Q.
88 |
89 |
90 | A.
91 |
92 |
93 |
94 |
95 |
96 | What is query execution time, and how can I optimize it?
97 |
98 |
99 |
100 |
101 | Query execution time refers to the duration it takes for a
102 | query to complete. To optimize query execution time, you
103 | can consider strategies such as indexing the appropriate
104 | columns, rewriting or optimizing the query logic,
105 | denormalizing data for better performance, or tuning the
106 | database configuration.
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | Q.
116 |
117 |
118 | A.
119 |
120 |
121 |
122 |
123 |
124 | What is query throughput, and how can I improve it?
125 |
126 |
127 |
128 |
129 | Query throughput refers to the number of queries that can
130 | be processed within a given timeframe. To improve query
131 | throughput, you can optimize the database schema, utilize
132 | caching mechanisms, distribute data across multiple
133 | servers, parallelize query execution, and implement query
134 | optimization techniques.
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | Q.
144 |
145 |
146 | A.
147 |
148 |
149 |
150 |
151 |
152 | What is query optimization, and how can I optimize my SQL
153 | queries?
154 |
155 |
156 |
157 |
158 | Query optimization involves improving the efficiency of
159 | SQL queries by selecting optimal execution plans. You can
160 | optimize SQL queries by using appropriate indexing,
161 | rewriting complex queries, avoiding unnecessary joins or
162 | subqueries, utilizing query hints, and analyzing and
163 | tuning the database configuration.
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Q.
173 |
174 |
175 | A.
176 |
177 |
178 |
179 |
180 |
181 | What are some common performance issues with SQL queries,
182 | and how can I address them?
183 |
184 |
185 |
186 |
187 | Common performance issues with SQL queries include slow
188 | query execution, high resource utilization, lack of
189 | indexing, inefficient join operations, and suboptimal
190 | query plans. You can address these issues by optimizing
191 | query logic, adding or modifying indexes, rewriting
192 | queries, and monitoring resource usage.
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 | >
202 | );
203 | };
204 |
205 | export default FAQ;
206 |
--------------------------------------------------------------------------------
/src/pages/homepage.tsx:
--------------------------------------------------------------------------------
1 | // import { type NextPage } from "next";
2 | import Header from '../components/Header';
3 | import MainContainer from '../containers/MainContainer';
4 | // import { useSession } from "next-auth/react";
5 | import Head from 'next/head';
6 | // import Link from "next/link";
7 | // import { api } from "~/utils/api";
8 |
9 | const Homepage: React.FC = () => {
10 | return (
11 | <>
12 |
13 | Query IQ Homepage
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | };
24 |
25 | export default Homepage;
26 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { type NextPage } from 'next';
2 | import Head from 'next/head';
3 | // import Link from 'next/link';
4 | import AuthShowcase from '~/components/AuthShowcase';
5 |
6 | const Home: NextPage = () => {
7 | return (
8 | <>
9 |
10 | Query IQ
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Query
19 | {' '}
20 | IQ
21 |
22 | {/*
23 |
28 |
First Steps →
29 |
30 | Just the basics - Everything you need to know to set up your
31 | database and authentication.
32 |
33 |
34 |
39 |
Documentation →
40 |
41 | Learn more about Create T3 App, the libraries it uses, and how
42 | to deploy it.
43 |
44 |
45 |
*/}
46 |
47 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default Home;
54 |
--------------------------------------------------------------------------------
/src/server/.env.example:
--------------------------------------------------------------------------------
1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to
2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date
3 | # when you add new variables to `.env`.
4 |
5 | # This file will be committed to version control, so make sure not to have any
6 | # secrets in it. If you are cloning this repo, create a copy of this file named
7 | # ".env" and populate it with your secrets.
8 |
9 | PORT=3001
--------------------------------------------------------------------------------
/src/server/auth.ts:
--------------------------------------------------------------------------------
1 | import { PrismaAdapter } from '@next-auth/prisma-adapter';
2 | import { type GetServerSidePropsContext } from 'next';
3 | import {
4 | getServerSession,
5 | type NextAuthOptions,
6 | type DefaultSession,
7 | } from 'next-auth';
8 | import GithubProvider from 'next-auth/providers/github';
9 | import { env } from '~/env.mjs';
10 | import { prisma } from '~/server/db';
11 |
12 | /**
13 | * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
14 | * object and keep type safety.
15 | *
16 | * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
17 | */
18 | declare module 'next-auth' {
19 | interface Session extends DefaultSession {
20 | user: {
21 | id: string;
22 | // ...other properties
23 | // role: UserRole;
24 | } & DefaultSession['user'];
25 | }
26 |
27 | // interface User {
28 | // // ...other properties
29 | // // role: UserRole;
30 | // }
31 | }
32 |
33 | /**
34 | * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
35 | *
36 | * @see https://next-auth.js.org/configuration/options
37 | */
38 | export const authOptions: NextAuthOptions = {
39 | callbacks: {
40 | session: ({ session, user }) => ({
41 | ...session,
42 | user: {
43 | ...session.user,
44 | id: user.id,
45 | },
46 | }),
47 | },
48 | adapter: PrismaAdapter(prisma),
49 | providers: [
50 | GithubProvider({
51 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
52 | clientId: process.env.GITHUB_ID!,
53 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54 | clientSecret: process.env.GITHUB_SECRET!,
55 | }),
56 | /**
57 | * ...add more providers here.
58 | *
59 | * @see https://next-auth.js.org/providers/github
60 | * @see https://authjs.dev/reference/adapter/prisma
61 | * @see https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/github.ts
62 | * @see https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
63 | * @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow
64 | */
65 | ],
66 | };
67 |
68 | /**
69 | * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
70 | *
71 | * @see https://next-auth.js.org/configuration/nextjs
72 | */
73 | export const getServerAuthSession = (ctx: {
74 | req: GetServerSidePropsContext['req'];
75 | res: GetServerSidePropsContext['res'];
76 | }) => {
77 | return getServerSession(ctx.req, ctx.res, authOptions);
78 | };
79 |
--------------------------------------------------------------------------------
/src/server/controllers/connectionController.ts:
--------------------------------------------------------------------------------
1 | import { RequestHandler, NextFunction } from 'express';
2 | import { Pool } from 'pg';
3 |
4 | type ConnectionController = {
5 | dbConnect: RequestHandler,
6 | createExtension: RequestHandler
7 | }
8 |
9 | const connectionController: ConnectionController = {
10 | dbConnect: async (req, res, next) => {
11 | console.log('dbConnect')
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 |
22 | try {
23 | const testQuery = await pool.query('SELECT version();')
24 | console.log(testQuery)
25 | res.locals.dbConnection = db;
26 | res.locals.result = {};
27 | return next();
28 |
29 | } catch(error) {
30 | return next({
31 | log: `ERROR caught in connectController.dbConnect: ${error}`,
32 | status: 400,
33 | message:
34 | 'ERROR: error has occured in connectController.dbConnect',
35 | })
36 | }
37 |
38 |
39 | },
40 |
41 | createExtension: async (req, res, next) => {
42 | const db = res.locals.dbConnection;
43 | const queryString = 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements';
44 |
45 | console.log('createExtension')
46 |
47 | const extExists = await db.query(`SELECT extname
48 | FROM pg_extension
49 | WHERE extname = 'pg_stat_statements';`)
50 |
51 | console.log(extExists)
52 |
53 | try {
54 | await db.query(queryString);
55 | res.locals.result.validURI = true;
56 | return next();
57 | } catch (error) {
58 | return next({
59 | log: `ERROR caught in connectController.createExtension: ${error}`,
60 | status: 400,
61 | message:
62 | 'ERROR: error has occured in connectController.createExtension',
63 | });
64 | }
65 | },
66 | }
67 |
68 | export default connectionController;
--------------------------------------------------------------------------------
/src/server/controllers/grafanaController.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { NextFunction, RequestHandler, Request, Response } from 'express';
3 | import { type FormData } from '../../types/types';
4 | import { dashBoardHelper } from './dashBoardHelper';
5 | import { pgQueryHelper } from './pgQueryHelper';
6 |
7 | interface GrafanaAPIHandler {
8 | (req: Request, res: Response, next: NextFunction): Promise;
9 | }
10 |
11 | type GrafanaController = {
12 | createDataSource: GrafanaAPIHandler;
13 | createDashBoard: GrafanaAPIHandler;
14 | getPgQueryMetrics: GrafanaAPIHandler;
15 | deleteDataSource: GrafanaAPIHandler;
16 | deleteDashBoard: GrafanaAPIHandler;
17 | deleteQueryDashBoard: GrafanaAPIHandler;
18 | };
19 |
20 | interface QueryPanelResponse {
21 | slug: string;
22 | status: string;
23 | uid: string;
24 | }
25 |
26 | const grafanaController: GrafanaController = {
27 | //createDataSource method receives grafana and PGDB information from frontend and
28 | //creates datasource on the users local grafana instance
29 | createDataSource: async (req: Request, res: Response, next: NextFunction) => {
30 | const {
31 | graf_name,
32 | graf_pass,
33 | graf_port,
34 | db_name,
35 | db_url,
36 | db_username,
37 | db_server,
38 | db_password,
39 | } = req.body as FormData;
40 |
41 | //the user's local grafana instance
42 | const url = `http://localhost:${graf_port}/api/datasources`;
43 |
44 | //request body sent to grafana local API, includes all the user PGDB information
45 | const body = {
46 | orgId: 1,
47 | name: `${db_name}`,
48 | type: 'postgres',
49 | typeLogoUrl: 'asd',
50 | access: 'proxy',
51 | url: `${db_url}`,
52 | user: `${db_username}`,
53 | database: `${db_server}`,
54 | basicAuth: false,
55 | basicAuthUser: `${graf_name}`,
56 | withCredentials: false,
57 | isDefault: true,
58 | jsonData: {
59 | maxOpenConns: 100,
60 | maxIdleConns: 100,
61 | maxIdleConnsAuto: true,
62 | connMaxLifetime: 14400,
63 | sslmode: 'disable',
64 | postgresVersion: 1500,
65 | },
66 | secureJsonFields: {},
67 | version: 2,
68 | readOnly: false,
69 | secureJsonData: {
70 | password: `${db_password}`,
71 | },
72 | };
73 |
74 | //headers for request to Graf API, grants basic auth to graf account
75 | const headers = {
76 | Accept: 'application/json',
77 | 'Content-Type': 'application/json',
78 | Authorization: `Basic ${Buffer.from(`${graf_name}:${graf_pass}`).toString(
79 | 'base64'
80 | )}`,
81 | };
82 |
83 | const payload = {
84 | method: 'POST',
85 | headers: headers,
86 | body: JSON.stringify(body),
87 | };
88 |
89 | try {
90 | const response = await fetch(url, payload);
91 | const data = (await response.json()) as Promise;
92 |
93 | //persist the response and port for next fetch in createDashBoard
94 | res.locals.data = data;
95 | res.locals.graf_port = graf_port;
96 | res.locals.headers = headers;
97 |
98 | // setTimeout(() => {
99 | // return next();
100 | // }, 5000);
101 |
102 | return next();
103 | } catch (error) {
104 | const errorMessage =
105 | // Ensure that what's being used in the template literal can indeed be converted to a string
106 | error instanceof Error ? error.message : String(error);
107 |
108 | return next({
109 | log: `${errorMessage}: error in the grafanaController.createDataSource`,
110 | status: 400,
111 | message: `${errorMessage}: error with the data source`,
112 | });
113 | }
114 | },
115 |
116 | //createDashBoard method sends POST request to Graf Local API to create a preconfigured dashboard
117 | //(see dashBoardHelper) that contains all the graphs and metrics we need for the iFrames on the frontend
118 | createDashBoard: async (req: Request, res: Response, next: NextFunction) => {
119 | const { graf_port, headers } = res.locals as {
120 | graf_port: string;
121 | headers: {
122 | Accept: string;
123 | 'Content-Type': string;
124 | Authorization: string;
125 | };
126 | };
127 |
128 | const url = `http://localhost:${graf_port}/api/dashboards/db`;
129 |
130 | //request body is created using helper function dashBoardHelper that inserts the uid from the data source creation
131 | //in all the areas necessary
132 | const body = dashBoardHelper(res.locals.data.datasource.uid);
133 |
134 | const payload = {
135 | method: 'POST',
136 | headers: headers,
137 | body: JSON.stringify(body),
138 | } as {
139 | method: string;
140 | headers: {
141 | Accept: string;
142 | 'Content-Type': string;
143 | Authorization: string;
144 | };
145 | body: string;
146 | };
147 |
148 | try {
149 | const response = await fetch(url, payload);
150 | const data = (await response.json()) as {
151 | slug: string;
152 | status: string;
153 | uid: string;
154 | };
155 |
156 | //creating an array of all the iFrame urls to pass to the frontend
157 | const urlArray = [];
158 | for (let i = 1; i <= body.dashboard.panels.length; i++) {
159 | urlArray.push(
160 | `http://localhost:3000/d-solo/${data.uid}/${data.slug}?orgId=1&refresh=30s&panelId=${i}`
161 | );
162 | }
163 |
164 | //response is stored in res.locals.dashboard to send the to the frontend
165 | res.locals.dashboard = {
166 | slug: data.slug,
167 | dashboarduid: data.uid,
168 | status: data.status,
169 | iFrames: urlArray,
170 | datasourceuid: res.locals.data.datasource.uid,
171 | } as {
172 | slug: string;
173 | dashboarduid: string;
174 | status: string;
175 | iFrames: string[];
176 | datasourceuid: string;
177 | };
178 | return next();
179 | } catch (error) {
180 | const errorMessage =
181 | // Ensure that what's being used in the template literal can indeed be converted to a string
182 | error instanceof Error ? error.message : String(error);
183 |
184 | return next({
185 | log: `${errorMessage}: error in the grafanaController.createDashBoard`,
186 | status: 400,
187 | message: `${errorMessage}: error with the data source`,
188 | });
189 | }
190 | },
191 |
192 | /**
193 | * @name getPgQueryMetrics
194 | * @description This function will get pg_stat_statement metrics from running an arbitrary query on a postgres database and return the URLs and URL metadata for the Grafana panel iFrames
195 | * @route /api/query
196 | * @param req {Object} req.body = {"query":"QUERY", "datasourceUID":"DATASOURCE_UID", "GrafanaCredentials":{"graf_name":"USERNAME","graf_port":"PORT","graf_pass":"PASSWORD"}}
197 | * @
198 | */
199 | getPgQueryMetrics: async (
200 | req: Request,
201 | res: Response,
202 | next: NextFunction
203 | ) => {
204 | const { query, datasourceUID, GrafanaCredentials } = req.body as {
205 | query: string;
206 | datasourceUID: string;
207 | GrafanaCredentials: {
208 | graf_port: string;
209 | graf_name: string;
210 | graf_pass: string;
211 | };
212 | };
213 |
214 | const url = `http://localhost:${GrafanaCredentials.graf_port}/api/dashboards/db`;
215 | const headers = {
216 | Accept: 'application/json',
217 | 'Content-Type': 'application/json',
218 | Authorization: `Basic ${Buffer.from(
219 | `${GrafanaCredentials.graf_name}:${GrafanaCredentials.graf_pass}`
220 | ).toString('base64')}`,
221 | };
222 | const queryPanels = pgQueryHelper(query, datasourceUID);
223 | const payload = {
224 | method: 'POST',
225 | headers: headers,
226 | body: JSON.stringify(queryPanels),
227 | };
228 |
229 | try {
230 | const response = await fetch(url, payload);
231 | const data = (await response.json()) as QueryPanelResponse;
232 |
233 | if (process.env.NODE_ENV === 'development') {
234 | console.log(data);
235 | }
236 |
237 | const urlArray: string[] = [];
238 |
239 | // Create array of iframe URLs for each Grafana panel
240 | for (let i = 1; i <= queryPanels.dashboard.panels.length; i++) {
241 | urlArray.push(
242 | `http://localhost:3000/d-solo/${data.uid}/${data.slug}?orgId=1&panelId=${i}`
243 | );
244 | }
245 |
246 | // Attach metadata needed to generate iframe URLs to response, this object will be sent to client upon POST to /api/query
247 | res.locals.queryPanels = {
248 | slug: data.slug,
249 | uid: data.uid,
250 | status: data.status,
251 | iFrames: urlArray,
252 | } as {
253 | slug: string;
254 | uid: string;
255 | status: string;
256 | iFrames: string[];
257 | };
258 |
259 | return next();
260 | } catch (error) {
261 | const errorMessage =
262 | // Ensure that what's being used in the template literal can indeed be converted to a string
263 | error instanceof Error ? error.message : String(error);
264 |
265 | return next({
266 | log: `${errorMessage}: error in the grafanaController.query`,
267 | status: 400,
268 | message: `${errorMessage}: error with the data source`,
269 | });
270 | }
271 | },
272 |
273 | deleteDataSource: async (req, res, next) => {
274 | const { dashboardUID, datasourceUID, GrafanaCredentials } = req.body as {
275 | dashboardUID: string;
276 | datasourceUID: string;
277 | GrafanaCredentials: {
278 | graf_port: string;
279 | graf_name: string;
280 | graf_pass: string;
281 | };
282 | };
283 |
284 | const { graf_name, graf_pass, graf_port } = GrafanaCredentials;
285 |
286 | const url = `http://localhost:${graf_port}/api/datasources/uid/${datasourceUID}`;
287 |
288 | const headers = {
289 | Accept: 'application/json',
290 | 'Content-Type': 'application/json',
291 | Authorization: `Basic ${Buffer.from(`${graf_name}:${graf_pass}`).toString(
292 | 'base64'
293 | )}`,
294 | };
295 |
296 | const payload = {
297 | method: 'DELETE',
298 | headers: headers,
299 | };
300 |
301 | try {
302 | const response = await fetch(url, payload);
303 | const data = (await response.json()) as Promise;
304 |
305 | res.locals.dashboardUID = dashboardUID;
306 | res.locals.GrafanaCredentials = GrafanaCredentials;
307 | res.locals.dataSourceResponse = data;
308 |
309 | return next();
310 | } catch (error) {
311 | const errorMessage =
312 | // Ensure that what's being used in the template literal can indeed be converted to a string
313 | error instanceof Error ? error.message : String(error);
314 |
315 | return next({
316 | log: `${errorMessage}: error in the grafanaController.deleteDataSource`,
317 | status: 400,
318 | message: `${errorMessage}: error with the data source UID`,
319 | });
320 | }
321 |
322 | },
323 |
324 | deleteDashBoard: async (req, res, next) => {
325 |
326 | const { graf_name, graf_pass, graf_port } = res.locals.GrafanaCredentials;
327 | const { dashboardUID } = res.locals;
328 |
329 | const url = `http://localhost:${graf_port}/api/dashboards/uid/${dashboardUID}`;
330 |
331 | const headers = {
332 | Accept: 'application/json',
333 | 'Content-Type': 'application/json',
334 | Authorization: `Basic ${Buffer.from(`${graf_name}:${graf_pass}`).toString(
335 | 'base64'
336 | )}`,
337 | };
338 |
339 | const payload = {
340 | method: 'DELETE',
341 | headers: headers,
342 | };
343 |
344 | try {
345 | const response = await fetch(url, payload);
346 | const data = (await response.json()) as Promise;
347 |
348 | res.locals.data = {datasource: res.locals.dataSourceResponse, dashboard: data}
349 |
350 | return next();
351 | } catch (error) {
352 | const errorMessage =
353 | // Ensure that what's being used in the template literal can indeed be converted to a string
354 | error instanceof Error ? error.message : String(error);
355 |
356 | return next({
357 | log: `${errorMessage}: error in the grafanaController.deleteDashboard`,
358 | status: 400,
359 | message: `${errorMessage}: error with the dashboard UID`,
360 | });
361 | }
362 | },
363 |
364 | deleteQueryDashBoard: async (req, res, next) => {
365 | const { dashboardUID, datasourceUID, GrafanaCredentials } = req.body as {
366 | dashboardUID: string;
367 | datasourceUID: string;
368 | GrafanaCredentials: {
369 | graf_port: string;
370 | graf_name: string;
371 | graf_pass: string;
372 | };
373 | };
374 |
375 | const { graf_name, graf_pass, graf_port } = GrafanaCredentials;
376 |
377 | const url = `http://localhost:${graf_port}/api/dashboards/uid/${dashboardUID}`;
378 |
379 | const headers = {
380 | Accept: 'application/json',
381 | 'Content-Type': 'application/json',
382 | Authorization: `Basic ${Buffer.from(`${graf_name}:${graf_pass}`).toString(
383 | 'base64'
384 | )}`,
385 | };
386 |
387 | const payload = {
388 | method: 'DELETE',
389 | headers: headers,
390 | };
391 |
392 | try {
393 | const response = await fetch(url, payload);
394 | const data = (await response.json()) as Promise;
395 |
396 | res.locals.data = data;
397 |
398 | return next();
399 | } catch (error) {
400 | const errorMessage =
401 | // Ensure that what's being used in the template literal can indeed be converted to a string
402 | error instanceof Error ? error.message : String(error);
403 |
404 | return next({
405 | log: `${errorMessage}: error in the grafanaController.deleteQueryDashBoard`,
406 | status: 400,
407 | message: `${errorMessage}: error with the query dashboard UID`,
408 | });
409 | }
410 | },
411 | };
412 |
413 | export default grafanaController;
414 |
--------------------------------------------------------------------------------
/src/server/controllers/pgQueryHelper.ts:
--------------------------------------------------------------------------------
1 | import {v4 as uuidv4} from 'uuid';
2 |
3 | export const pgQueryHelper = (userQuery: string, datasourceUID: string) => {
4 | const queryPanels = {
5 | dashboard: {
6 | __inputs: [
7 | {
8 | name: 'Query',
9 | label: 'Query',
10 | description: '',
11 | type: 'datasource',
12 | pluginId: 'postgres',
13 | pluginName: 'PostgreSQL',
14 | },
15 | ],
16 | __elements: {},
17 | __requires: [
18 | {
19 | type: 'panel',
20 | id: 'barchart',
21 | name: 'Bar chart',
22 | version: '',
23 | },
24 | {
25 | type: 'panel',
26 | id: 'bargauge',
27 | name: 'Bar gauge',
28 | version: '',
29 | },
30 | {
31 | type: 'grafana',
32 | id: 'grafana',
33 | name: 'Grafana',
34 | version: '10.0.1',
35 | },
36 | {
37 | type: 'datasource',
38 | id: 'postgres',
39 | name: 'PostgreSQL',
40 | version: '1.0.0',
41 | },
42 | {
43 | type: 'panel',
44 | id: 'table',
45 | name: 'Table',
46 | version: '',
47 | },
48 | ],
49 | annotations: {
50 | list: [
51 | {
52 | builtIn: 1,
53 | datasource: {
54 | type: 'grafana',
55 | uid: '-- Grafana --',
56 | },
57 | enable: true,
58 | hide: true,
59 | iconColor: 'rgba(0, 211, 255, 1)',
60 | name: 'Annotations & Alerts',
61 | type: 'dashboard',
62 | },
63 | ],
64 | },
65 | editable: true,
66 | fiscalYearStartMonth: 0,
67 | graphTooltip: 0,
68 | id: null,
69 | links: [],
70 | liveNow: false,
71 | panels: [
72 | {
73 | datasource: {
74 | type: 'postgres',
75 | uid: `${datasourceUID}`,
76 | },
77 | description:
78 | 'The following data is from EXPLAIN ANALYZE, which provides execution plan and performance query for specific query input including: \n\nNode Type: The type of node or operation being performed, such as "Seq Scan," "Index Scan," "Hash Join," etc.\n\nRelation Name: The name of the relation or table being accessed.\n\nStartup Cost: The estimated cost of starting up the operation.\n\nTotal Cost: The estimated total cost of executing the operation.\n\nRows: The estimated number of rows output by the operation.\n\nWidth: The estimated average width of each row in bytes.\n\nActual Startup Time: The actual time spent on starting up the operation during execution.\n\nActual Total Time: The actual total time taken to execute the operation.\n\nActual Rows: The actual number of rows output by the operation during execution.\n\nActual Loops: The number of times the operation was executed (loops) during execution.\n\nThese are some of the common data types provided by EXPLAIN ANALYZE. The actual values and additional columns may vary depending on the specific query, the execution plan, and the available statistics.',
79 | fieldConfig: {
80 | defaults: {
81 | custom: {
82 | align: 'auto',
83 | cellOptions: {
84 | type: 'auto',
85 | },
86 | inspect: false,
87 | },
88 | mappings: [],
89 | thresholds: {
90 | mode: 'absolute',
91 | steps: [
92 | {
93 | color: 'green',
94 | value: null,
95 | },
96 | {
97 | color: 'red',
98 | value: 80,
99 | },
100 | ],
101 | },
102 | unit: 's',
103 | },
104 | overrides: [],
105 | },
106 | gridPos: {
107 | h: 8,
108 | w: 8,
109 | x: 0,
110 | y: 0,
111 | },
112 | id: 1,
113 | options: {
114 | cellHeight: 'md',
115 | footer: {
116 | countRows: false,
117 | fields: '',
118 | reducer: ['sum'],
119 | show: false,
120 | },
121 | showHeader: true,
122 | },
123 | pluginVersion: '10.0.1',
124 | targets: [
125 | {
126 | datasource: {
127 | type: 'postgres',
128 | uid: `${datasourceUID}`,
129 | },
130 | editorMode: 'code',
131 | format: 'table',
132 | rawQuery: true,
133 | rawSql: `EXPLAIN ANALYZE ${userQuery};`,
134 | refId: 'A',
135 | sql: {
136 | columns: [
137 | {
138 | parameters: [],
139 | type: 'function',
140 | },
141 | ],
142 | groupBy: [
143 | {
144 | property: {
145 | type: 'string',
146 | },
147 | type: 'groupBy',
148 | },
149 | ],
150 | limit: 50,
151 | },
152 | },
153 | ],
154 | title: 'Query Performance Metrics ',
155 | type: 'table',
156 | },
157 | // {
158 | // datasource: {
159 | // type: 'postgres',
160 | // uid: `${datasourceUID}`,
161 | // },
162 | // description:
163 | // 'This chart displays the total execution time in seconds. Total execution time measures the time taken to both planning and execution of the query. Query calls refer to the number of times a particular query has been executed since the pg_stat_statements extension was enabled or since the statistics were last reset.',
164 | // fieldConfig: {
165 | // defaults: {
166 | // color: {
167 | // mode: 'continuous-BlYlRd',
168 | // },
169 | // custom: {
170 | // axisCenteredZero: false,
171 | // axisColorMode: 'text',
172 | // axisLabel: '',
173 | // axisPlacement: 'auto',
174 | // fillOpacity: 80,
175 | // gradientMode: 'hue',
176 | // hideFrom: {
177 | // legend: false,
178 | // tooltip: false,
179 | // viz: false,
180 | // },
181 | // lineWidth: 1,
182 | // scaleDistribution: {
183 | // type: 'linear',
184 | // },
185 | // thresholdsStyle: {
186 | // mode: 'off',
187 | // },
188 | // },
189 | // mappings: [],
190 | // thresholds: {
191 | // mode: 'absolute',
192 | // steps: [
193 | // {
194 | // color: 'green',
195 | // value: null,
196 | // },
197 | // {
198 | // color: 'red',
199 | // value: 80,
200 | // },
201 | // ],
202 | // },
203 | // unit: 's',
204 | // },
205 | // overrides: [],
206 | // },
207 | // gridPos: {
208 | // h: 8,
209 | // w: 9,
210 | // x: 8,
211 | // y: 0,
212 | // },
213 | // id: 3,
214 | // options: {
215 | // barRadius: 0,
216 | // barWidth: 0.97,
217 | // fullHighlight: false,
218 | // groupWidth: 0.7,
219 | // legend: {
220 | // calcs: [],
221 | // displayMode: 'list',
222 | // placement: 'right',
223 | // showLegend: true,
224 | // },
225 | // orientation: 'auto',
226 | // showValue: 'never',
227 | // stacking: 'none',
228 | // tooltip: {
229 | // mode: 'single',
230 | // sort: 'none',
231 | // },
232 | // xTickLabelRotation: 0,
233 | // xTickLabelSpacing: 0,
234 | // },
235 | // pluginVersion: '10.0.1',
236 | // targets: [
237 | // {
238 | // datasource: {
239 | // type: 'postgres',
240 | // uid: `${datasourceUID}`,
241 | // },
242 | // editorMode: 'code',
243 | // format: 'table',
244 | // rawQuery: true,
245 | // rawSql: `SELECT total_exec_time, calls, query FROM pg_stat_statements\nWHERE query LIKE '%${userQuery}%'\n AND query NOT LIKE '%EXPLAIN%';`,
246 | // refId: 'A',
247 | // sql: {
248 | // columns: [
249 | // {
250 | // parameters: [],
251 | // type: 'function',
252 | // },
253 | // ],
254 | // groupBy: [
255 | // {
256 | // property: {
257 | // type: 'string',
258 | // },
259 | // type: 'groupBy',
260 | // },
261 | // ],
262 | // limit: 50,
263 | // },
264 | // },
265 | // ],
266 | // title: 'Total Execution Time and Calls',
267 | // type: 'barchart',
268 | // },
269 | // {
270 | // datasource: {
271 | // type: 'postgres',
272 | // uid: `${datasourceUID}`,
273 | // },
274 | // description:
275 | // 'This chart displays shared buffer hits and shared buffer read measured in bigInt. Shared buffer hits represents the number of shared buffer hits. The shared buffer in PostgreSQL is a cache area in memory where frequently accessed data blocks from the database are stored. Therefore, this number indicates the efficiency of cache utilization for the given query. Higher values for shared buffer hits suggest that the data blocks required by the query are frequently found in the shared buffer, resulting in improved performance.\n\nHigher values for shared buffer read suggest that the query required data blocks that were not present in the shared buffer, leading to disk reads. In general, minimizing disk reads by maximizing shared buffer hits shared buffer hits is beneficial for query performance.',
276 | // fieldConfig: {
277 | // defaults: {
278 | // color: {
279 | // mode: 'continuous-BlYlRd',
280 | // },
281 | // mappings: [],
282 | // thresholds: {
283 | // mode: 'absolute',
284 | // steps: [
285 | // {
286 | // color: 'green',
287 | // // value: null,
288 | // // },
289 | // // {
290 | // // color: 'red',
291 | // value: 80,
292 | // },
293 | // ],
294 | // },
295 | // unit: 'bytes',
296 | // },
297 | // overrides: [],
298 | // },
299 | // gridPos: {
300 | // h: 9,
301 | // w: 8,
302 | // x: 0,
303 | // y: 8,
304 | // },
305 | // id: 4,
306 | // options: {
307 | // displayMode: 'gradient',
308 | // minVizHeight: 10,
309 | // minVizWidth: 0,
310 | // orientation: 'horizontal',
311 | // reduceOptions: {
312 | // calcs: ['lastNotNull'],
313 | // fields: '',
314 | // values: false,
315 | // },
316 | // showUnfilled: true,
317 | // valueMode: 'color',
318 | // },
319 | // pluginVersion: '10.0.1',
320 | // targets: [
321 | // {
322 | // datasource: {
323 | // type: 'postgres',
324 | // uid: `${datasourceUID}`,
325 | // },
326 | // editorMode: 'code',
327 | // format: 'table',
328 | // rawQuery: true,
329 | // rawSql: `SELECT shared_blks_hit AS "Shared Buffer Hits", shared_blks_read AS "Shared Buffer Read" FROM pg_stat_statements\nWHERE query LIKE \'%${userQuery}%\'\n AND query NOT LIKE \'%EXPLAIN%\';`,
330 | // refId: 'A',
331 | // sql: {
332 | // columns: [
333 | // {
334 | // parameters: [],
335 | // type: 'function',
336 | // },
337 | // ],
338 | // groupBy: [
339 | // {
340 | // property: {
341 | // type: 'string',
342 | // },
343 | // type: 'groupBy',
344 | // },
345 | // ],
346 | // limit: 50,
347 | // },
348 | // },
349 | // ],
350 | // title: 'Shared Buffer Hits vs Shared Buffer Read',
351 | // type: 'bargauge',
352 | // },
353 | ],
354 | refresh: '',
355 | schemaVersion: 38,
356 | style: 'dark',
357 | tags: [],
358 | templating: {
359 | list: [],
360 | },
361 | time: {
362 | from: 'now-6h',
363 | to: 'now',
364 | },
365 | timepicker: {},
366 | timezone: '',
367 | title: `Query Dashboard ${uuidv4()}`,
368 | uid: null,
369 | version: 3,
370 | weekStart: '',
371 | },
372 | };
373 | return queryPanels;
374 | };
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import { env } from "~/env.mjs";
3 |
4 | const globalForPrisma = globalThis as unknown as {
5 | prisma: PrismaClient | undefined;
6 | };
7 |
8 | export const prisma =
9 | globalForPrisma.prisma ??
10 | new PrismaClient({
11 | log:
12 | env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
13 | });
14 |
15 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
16 |
--------------------------------------------------------------------------------
/src/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "dotenv": "^16.3.1"
13 | }
14 | },
15 | "node_modules/dotenv": {
16 | "version": "16.3.1",
17 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
18 | "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
19 | "engines": {
20 | "node": ">=12"
21 | },
22 | "funding": {
23 | "url": "https://github.com/motdotla/dotenv?sponsor=1"
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "dotenv": "^16.3.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/server/routers/apiRouter.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-misused-promises */
2 | import { Router } from 'express';
3 | import grafanaController from '../controllers/grafanaController';
4 |
5 | const router = Router();
6 |
7 | router.post(
8 | '/connect',
9 | grafanaController.createDataSource,
10 | grafanaController.createDashBoard,
11 | (req, res) => {
12 | return res.status(200).json(res.locals.dashboard);
13 | }
14 | );
15 |
16 | router.post('/query', grafanaController.getPgQueryMetrics, (req, res) => {
17 | return res.status(200).json(res.locals.queryPanels);
18 | });
19 |
20 | router.delete(
21 | '/disconnect',
22 | grafanaController.deleteDataSource,
23 | grafanaController.deleteDashBoard,
24 | (req, res) => {
25 | return res.status(200).send(res.locals.data);
26 | });
27 |
28 | router.delete(
29 | '/delete',
30 | grafanaController.deleteQueryDashBoard,
31 | (req, res) => {
32 | return res.status(200).send(res.locals.data);
33 | });
34 |
35 | export default router;
36 |
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | import express, {
2 | type Application,
3 | type NextFunction,
4 | type Request,
5 | type Response,
6 | } from 'express';
7 | import next from 'next';
8 | import apiRouter from './routers/apiRouter';
9 | import cors from 'cors';
10 |
11 | // Required to pipe env variables into Express
12 | import dotenv from 'dotenv';
13 | dotenv.config();
14 |
15 | // Attach port to PORT_EXPRESS, which should be set to 3001, or 3003-3999.
16 | // If you see port running on 3002 during server start-up, you know that .env is not being loaded properly.
17 | const port = process.env.PORT_EXPRESS || 3002;
18 |
19 | const app: Application = express();
20 |
21 | // The following code creates a new Next.js application and initializes it.
22 | const dev = process.env.NODE_ENV !== 'production';
23 | const nextApp = next({ dev });
24 | const handle = nextApp.getRequestHandler();
25 |
26 | // This code sets up the server for the Next.js application.
27 | // It defines a route handler for all requests that come into the server.
28 | // The route handler uses the Next.js request handler to render the appropriate page.
29 | nextApp
30 | .prepare()
31 | .then(() => {
32 | app.get('*', (req: Request, res: Response) => {
33 | handle(req, res).catch((error) => {
34 | console.error(error);
35 | res.status(500).send('Error occured during Next.js message handling');
36 | });
37 | });
38 | })
39 | .catch((err: Error): void => {
40 | console.error(err);
41 | process.exit(1);
42 | });
43 |
44 | // Sets up the server to parse JSON
45 | app.use(express.json());
46 |
47 | // This code allows the express server to communicate with other servers and allows for the use of the cors package to communicate with the front end.
48 | app.use(express.urlencoded({ extended: true }));
49 | app.use(cors({ origin: 'http://localhost:3333' }));
50 |
51 | // This is the main router for the server
52 | // It handles all the API requests
53 | app.use('/api', apiRouter);
54 |
55 | // This provides some test routes on the Express server when in development mode. Otherwise, does nothing.
56 | if (process.env.NODE_ENV === 'development') {
57 | app.get('/', (req: Request, res: Response) => {
58 | res.send('Hello World!');
59 | console.log('/ endpoint hit');
60 | });
61 |
62 | app.get('/user', (req: Request, res: Response) => {
63 | res.send('Hello from /user');
64 | console.log('/user endpoint hit');
65 | });
66 | }
67 |
68 | //catch all to non-existent routes
69 | app.use('*', (req: Request, res: Response) => {
70 | res.status(404).send('Page not found')
71 | })
72 |
73 | // Global error handler
74 | app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
75 | const defaultErr = {
76 | log: 'Express global error handler caught unknown middleware error',
77 | status: 500,
78 | message: { err: 'An error occurred' },
79 | };
80 | const errorObj = Object.assign({}, defaultErr, err);
81 | console.log(errorObj.log);
82 | return res.status(errorObj.status).json(errorObj.message);
83 | });
84 |
85 | app.listen(port, () =>
86 | console.log(`> Express OK, served at http://localhost:${port}`)
87 | );
88 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | box-sizing: border-box;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | body {
12 | /*background-image: linear-gradient(to bottom, #0f0f0f, #1f1f1f);*/
13 | background: #1f1f1f;
14 | height: 100dvh;
15 | /* Replace 'your-image-url' with the actual URL of your PNG image on GitHub */
16 | /* background-image: url('https://user-images.githubusercontent.com/108748353/256665415-02a9be39-db1b-4058-8d8e-47e9ea3fab57.png'); */
17 | /* Add additional background properties if needed */
18 | background-size: cover;
19 | background-position: center;
20 | background-repeat: no-repeat;
21 | }
22 |
23 | @layer utilities {
24 | .h-100dvh {
25 | height: 100dvh;
26 | }
27 | .mb-128 {
28 | margin-bottom: 36rem;
29 | }
30 | .outline-red {
31 | outline: 1px solid red;
32 | }
33 | .outline-blue {
34 | outline: 1px solid cyan;
35 | }
36 | .outline-green {
37 | outline: 1px solid chartreuse;
38 | }
39 | .outline-purple {
40 | outline: 1px solid magenta;
41 | }
42 | }
43 |
44 | .background-container {
45 | background-image:
46 | url('https://user-images.githubusercontent.com/108748353/256665415-02a9be39-db1b-4058-8d8e-47e9ea3fab57.png');
47 | /* Add additional background properties if needed */
48 | /* background-size: 5; */
49 | background-position: top ;
50 | /* background-repeat: no-repeat; */
51 |
52 | }
53 |
54 | .background-container::before {
55 | content: '';
56 | position: absolute;
57 | top: 0;
58 | left: 0;
59 | width: 100%;
60 | height: 100%;
61 | /* Your scrolling background image */
62 | background-image: url('https://user-images.githubusercontent.com/108748353/256679338-7ec11ad9-4ec4-4506-8695-defba5e40aab.png');
63 | /* Add additional background properties if needed */
64 | background-size: cover;
65 | background-position: center;
66 | /* background-repeat: no-repeat; */
67 | opacity: 0.5; /* Adjust the opacity as desired */
68 | z-index: -1; /* Ensure the pseudo-element is behind the content */
69 | }
--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------
1 | import type { ChangeEvent } from 'react';
2 |
3 | export type GrafanaUserObject = {
4 | graf_name: string;
5 | graf_pass: string;
6 | graf_port: string;
7 | }
8 |
9 | export type FormData = {
10 | graf_name: string;
11 | graf_pass: string;
12 | graf_port: string;
13 | db_name: string;
14 | db_url: string;
15 | db_username: string;
16 | db_server: string;
17 | db_password: string;
18 | };
19 |
20 | export type dbUid = {
21 | datasourceUid: string;
22 | dashboardUid: string;
23 | }
24 |
25 | // parent container
26 | export type SideBarContainerProps = {
27 | openModal: React.Dispatch>;
28 | connection: boolean;
29 | formData: FormData;
30 | setFormData: React.Dispatch>;
31 | queryLog: QueryLogItemObject[];
32 | setQueryLog: React.Dispatch>>;
33 | editQueryLabel: (index: number, label: string) => void;
34 | deleteQuery: (index: number) => Promise;
35 | activeQuery: QueryLogItemObject;
36 | setActiveQuery: React.Dispatch>;
37 | setDashboardState: React.Dispatch>;
38 | disconnectDB: () => Promise;
39 | };
40 |
41 | // child of SideBarContainer
42 | export type DBConnectProps = {
43 | openModal: React.Dispatch>;
44 | connection: boolean;
45 | formData: FormData;
46 | setFormData: React.Dispatch>;
47 | disconnectDB: () => Promise;
48 | };
49 |
50 | // child of SideBarContainer
51 | export type QueryLogProps = {
52 | queryLog: QueryLogItemObject[];
53 | editQueryLabel: (index: number, label: string) => void;
54 | deleteQuery: (index: number) => Promise;
55 | activeQuery: QueryLogItemObject;
56 | setActiveQuery: React.Dispatch>;
57 | setDashboardState: React.Dispatch>;
58 | };
59 |
60 | // child of QueryLog
61 | export type QueryLogItemProps = {
62 | index: number;
63 | editQueryLabel: (index: number, label: string) => void;
64 | deleteQuery: (index: number) => Promise;
65 | queryLogObject: QueryLogItemObject;
66 | setActiveQuery: React.Dispatch>;
67 | activeQuery: QueryLogItemObject;
68 | setDashboardState: React.Dispatch>;
69 | };
70 |
71 | // shape of query log items data
72 | export type QueryLogItemObject = {
73 | query: string;
74 | data: string[];
75 | name: string;
76 | dashboardUID: string;
77 | };
78 |
79 | // Parent container
80 | export type QueryContainerProps = {
81 | setQueryLog: React.Dispatch>>;
82 | setQuery: React.Dispatch>;
83 | query: string;
84 | activeQuery: QueryLogItemObject;
85 | setActiveQuery: React.Dispatch>;
86 | dashboardState: string;
87 | setDashboardState: React.Dispatch>;
88 | databaseGraphs: string[];
89 | connection: boolean;
90 | grafanaUser: GrafanaUserObject;
91 | dbUid: dbUid;
92 | };
93 |
94 | // child of QueryContainer
95 | export type InputQueryProps = {
96 | setQueryLog: React.Dispatch>>;
97 | setQuery: React.Dispatch>;
98 | query: string;
99 | setActiveQuery: React.Dispatch>;
100 | setDashboardState: React.Dispatch>;
101 | activeQuery: QueryLogItemObject;
102 | grafanaUser: GrafanaUserObject;
103 | dbUid: dbUid;
104 | connection: boolean;
105 | };
106 |
107 | // child of InputQuery
108 | export type LoadingBarProps = {
109 | loadingProgress: number;
110 | };
111 |
112 | // parent container
113 | export type DashboardContainerProps = {
114 | activeQuery: QueryLogItemObject;
115 | dashboardState: string;
116 | setDashboardState: React.Dispatch>;
117 | databaseGraphs: string[];
118 | connection: boolean;
119 | };
120 |
121 | // parent modal
122 | export type DBModalProps = {
123 | openModal: React.Dispatch>;
124 | setFormData: React.Dispatch>;
125 | formData: FormData;
126 | isFormValid: boolean;
127 | handleConnect: React.MouseEventHandler;
128 | setGrafanaUser: React.Dispatch>;
129 | grafanaUser: GrafanaUserObject;
130 | };
131 |
132 | // child of DBModal
133 | export type DBSelectionProps = {
134 | handleCancel: () => void;
135 | handleClick: () => void;
136 | };
137 |
138 | // child of DBModal
139 | export type GrafanaCredentialsProps = {
140 | handleCancel: () => void;
141 | handleClick: () => void;
142 | formData: FormData;
143 | setFormData: React.Dispatch>;
144 | setGrafanaUser: React.Dispatch>;
145 | grafanaUser: GrafanaUserObject;
146 | };
147 |
148 | // child of DBModal
149 | export type DBCredentialsProps = {
150 | formData: FormData;
151 | setFormData: React.Dispatch>;
152 | handleConnect: React.MouseEventHandler;
153 | isFormValid: boolean;
154 | handleCancel: () => void;
155 | };
156 |
157 | // input for modals
158 | export type ModalFormInputProps = {
159 | placeholder: string;
160 | value: string;
161 | type: string;
162 | onChange: (e: ChangeEvent) => void;
163 | };
164 |
165 | // graph cards
166 | export type GraphCardProps = {
167 | src: string;
168 | key: number;
169 | };
170 |
--------------------------------------------------------------------------------
/src/utils/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QueryIQ/748c864eea9d810477567d13aa18309b4d95ce63/src/utils/.gitkeep
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { type Config } from 'tailwindcss';
2 | /** @type {import('tailwindcss').Config} */
3 | export default {
4 | content: ['./src/**/*.{js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | 'reem-kufi': ['Reem Kufi', 'sans-serif'],
9 | },
10 | },
11 | },
12 | plugins: [],
13 | } satisfies Config;
14 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules", "landing"]
3 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "checkJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "commonjs",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "noUncheckedIndexedAccess": true,
19 | "noImplicitAny": false,
20 | "baseUrl": ".",
21 | "paths": {
22 | "~/*": ["./src/*"]
23 | },
24 | "types": ["node", "next", "next/types/global", "jest"],
25 | "outDir": "./dist"
26 | },
27 | "include": [
28 | ".eslintrc.cjs",
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | "**/*.cjs",
33 | "**/*.mjs"
34 | ],
35 | "exclude": ["node_modules"]
36 | }
37 |
--------------------------------------------------------------------------------