├── .github
└── workflows
│ └── cypress_testing.yml
├── .gitignore
├── .vscode
├── launch.json
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── (root)
│ ├── Login
│ │ ├── AnimateLogin.tsx
│ │ ├── Login.tsx
│ │ └── page.tsx
│ ├── Register
│ │ ├── Register.tsx
│ │ └── page.tsx
│ ├── RootContainer.tsx
│ ├── frontendTypes.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── Main
│ ├── .DS_Store
│ ├── About
│ │ └── page.tsx
│ ├── Chart
│ │ ├── (components)
│ │ │ ├── AboutPage.tsx
│ │ │ ├── Chart.tsx
│ │ │ ├── DisplayContainer.tsx
│ │ │ ├── LoadContainer.tsx
│ │ │ ├── LoadItem.tsx
│ │ │ ├── MainContainer.tsx
│ │ │ ├── NavigationBar.tsx
│ │ │ ├── Resolver.tsx
│ │ │ ├── ResolverDisplay.tsx
│ │ │ ├── SaveContainer.tsx
│ │ │ ├── Schema.tsx
│ │ │ ├── VisualizeDB.tsx
│ │ │ └── VisualizeSchemaResolver.tsx
│ │ ├── (flow)
│ │ │ ├── Edges.tsx
│ │ │ ├── Flow.tsx
│ │ │ ├── Nodes.tsx
│ │ │ ├── TableNode.tsx
│ │ │ ├── TableRow.tsx
│ │ │ └── dummyRes.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── loading.tsx
└── types.ts
├── codingStandards.txt
├── cypress.config.ts
├── cypress
├── e2e
│ ├── root-to-main.cy.ts
│ ├── root.cy.ts
│ └── unit-testing.cy.ts
├── fixtures
│ └── example.json
├── support
│ ├── commands.ts
│ └── e2e.ts
└── unit-testing
│ └── unit-testing.cy.ts
├── docker-compose.yml
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pages
└── api
│ └── graphql.ts
├── postcss.config.js
├── public
├── andres.jpeg
├── brian.png
├── daniel.png
├── logo.png
└── stephen.png
├── server
├── .DS_Store
├── Dockerfile
├── db
│ ├── dbConnection.ts
│ └── quilDBConnection.ts
├── graphql
│ ├── models
│ │ ├── resolvers
│ │ │ └── query.resolver.ts
│ │ └── schemas
│ │ │ ├── Data.ts
│ │ │ ├── Mutation.ts
│ │ │ ├── Query.ts
│ │ │ └── Signin.ts
│ └── modelsSetup.ts
├── helperFunctions.ts
├── middleware
│ ├── auth.ts
│ └── userController.ts
├── package-lock.json
├── package.json
├── resolverGenerator.ts
├── schemaGenerator.ts
├── server.ts
├── tsconfig.json
└── types.ts
├── tailwind.config.js
└── tsconfig.json
/.github/workflows/cypress_testing.yml:
--------------------------------------------------------------------------------
1 | name: End-to-end tests
2 | on: [push]
3 | jobs:
4 | cypress-run:
5 | runs-on: ubuntu-20.04
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v3
9 | - run: npm i
10 | # Install NPM dependencies, cache them correctly
11 | # and run all Cypress tests
12 | - name: Cypress run
13 | uses: cypress-io/github-action@v5
14 | with:
15 | build: npm run build
16 | start: npm start
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 |
13 | # next
14 | .next/
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # TypeScript v1 declaration files
48 | typings/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 |
81 | # Next.js build output
82 | .next
83 |
84 | # Nuxt.js build / generate output
85 | .nuxt
86 | dist
87 |
88 | # Gatsby files
89 | .cache/
90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
91 | # https://nextjs.org/blog/next-9-1#public-directory-support
92 | # public
93 |
94 | # vuepress build output
95 | .vuepress/dist
96 |
97 | # Serverless directories
98 | .serverless/
99 |
100 | # FuseBox cache
101 | .fusebox/
102 |
103 | # DynamoDB Local files
104 | .dynamodb/
105 |
106 | # TernJS port file
107 | .tern-port
108 |
109 | .idea
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 |
9 |
10 | {
11 | "type": "node",
12 | "request": "launch",
13 | "name": "Launch Chrome against localhost",
14 | "url": "http://localhost:8080",
15 | "webRoot": "${workspaceFolder}"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.17.1
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json /usr/src/app/
6 |
7 | RUN npm install && npm install typescript -g
8 |
9 | COPY . /usr/src/app/
10 |
11 | RUN tsc
12 |
13 | RUN npm run build
14 |
15 | CMD npm start
16 |
17 | EXPOSE 3000
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QuiL - Writing GraphQL for you
2 |
3 | QuiL is an open source developer tool that simplfies the process of implementing GraphQL and helps engineers better understand their data base.
4 |
5 | ### Table of Contents
6 |
7 | -General Information
8 | -Technologies Used
9 | -Screenshots
10 | -How to use
11 | -To do
12 | -Contributors
13 |
14 | ### GraphQL Schema & Resolver Generator
15 |
16 | Powered by QuiL's database analyzation algorithm, QuiL is able to use a database connection string and produce nessecary GQL schemas and GQL resolvers a developer would need to start a GraphQL backend
17 |
18 | ### Technologies Used
19 |
20 | -Next.js 13
21 | -React.js (React Hooks) - v18.2.0
22 | -React-flow-renderer - v11.2.0
23 | -Express - v4.18.2
24 | -jest - v29.3.1
25 | -Cypress v11.2.0
26 | -supertest - v6.1.6
27 | -Docker
28 | -Tailwindcss - v3.2.2
29 |
30 | ### How to use
31 |
32 | 1. On the root page you will have the option to input your PostgreSQL database URI or a sample database.
33 | 2. The main page will have the database of your choice rendered.
34 | 3. Center of the dev tool you will be able to visualize and interact with the database.
35 | 4. Above the visualizer off to the right you will have three buttons.
36 | a. First field allows you to input a PostgreSQL URI followed by a [Launch] button.
37 | b. Second button [Save] will give you the opportunity to save this project for future use.
38 | c. Third button [Load] lets you access previously saved projects.
39 | 5. On the top-left hand side you will have a [View Schemas/Resolvers] button that will open a drawer that gives you access to the generated schema types and resolvers generated by QuiL.
40 |
41 | ### To Do
42 |
43 | -Test metrics
44 |
45 | -Be able to use more SQL databases then onto NoSQL.
46 |
47 | -Integrate Redis caching.
48 |
49 |
50 |
51 |
52 | ### Contributors
53 |
54 | -Brian Tran
55 |
56 | -Stephen Fitzsimmons
57 |
58 | -Daniel An
59 |
60 | -Andres Jaramillo
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/(root)/Login/AnimateLogin.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { motion } from "framer-motion";
3 | const AnimationLogin = () => {
4 | return (
5 |
6 |
7 |
13 | Login to
14 |
15 |
20 |
26 | view
27 |
28 |
29 |
35 | QuiL
36 |
37 |
38 |
46 |
55 | Databases
56 |
57 |
58 |
66 |
75 | Schemas
76 |
77 |
78 |
79 |
87 |
96 | Resolvers
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default AnimationLogin;
104 |
--------------------------------------------------------------------------------
/app/(root)/Login/Login.tsx:
--------------------------------------------------------------------------------
1 | import { inputObj, userObj } from '../../(root)/frontendTypes';
2 | import Link from 'next/link';
3 | import { motion } from 'framer-motion';
4 | import { useRouter } from 'next/navigation';
5 | import AnimationLogin from './AnimateLogin';
6 | const randomstring = require('randomstring');
7 |
8 | const Login = () => {
9 | const router = useRouter();
10 |
11 | const SIGNIN_STATE_CODE = 'c2lnbmlu';
12 |
13 | const loginHandler = async (e: any) => {
14 | e.preventDefault();
15 | const userObj: userObj = {
16 | username: e.target.username.value,
17 | password: e.target.password.value,
18 | };
19 |
20 | let data = await fetch('/api/graphql', {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | },
25 | body: JSON.stringify({
26 | query: `mutation {
27 | signin(username: "${userObj.username}", password: "${userObj.password}") {
28 | token
29 | }
30 | }`,
31 | }),
32 | })
33 | .then(data => {
34 | return data.json();
35 | })
36 | .then(data => {
37 | console.log('data', data);
38 |
39 | localStorage.setItem('token', data.data.signin.token);
40 | router.push('/');
41 | });
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
66 |
98 | OR
99 |
108 |
109 |
113 | Login with Github
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default Login;
123 |
--------------------------------------------------------------------------------
/app/(root)/Login/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Login from './Login';
4 | import { useEffect, useState } from 'react';
5 | import { useRouter } from 'next/navigation';
6 |
7 | export default function Page({ searchParams }: any) {
8 | const [code, setCode] = useState(searchParams.code);
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | if (code) {
13 | const handleOAuth = async (code: string) => {
14 | const oauthResponse = await fetch('/api/graphql', {
15 | method: 'POST',
16 | headers: {
17 | 'Content-Type': 'application/json',
18 | },
19 | body: JSON.stringify({
20 | query: `mutation {
21 | postOAuth(code: "${code}", oauthType: "signin") {
22 | token
23 | }
24 | }`,
25 | }),
26 | }).then(res => res.json());
27 | localStorage.setItem('token', oauthResponse.data.postOAuth.token);
28 | router.push('/');
29 | };
30 | handleOAuth(code);
31 | }
32 | }, [code]);
33 |
34 | if (code) {
35 | return (
36 |
37 |
Authorizing OAuth
38 |
39 | );
40 | }
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/app/(root)/Register/Register.tsx:
--------------------------------------------------------------------------------
1 | import { inputObj, userObj } from '../../(root)/frontendTypes';
2 | import { motion } from 'framer-motion';
3 | import Link from 'next/link';
4 | import { useRouter } from 'next/navigation';
5 | const randomstring = require('randomstring');
6 |
7 | const Register = () => {
8 | const router = useRouter();
9 | const REGISTER_STATE_CODE = 'cmVnaXN0ZXI';
10 |
11 | const createUserHandler = async (e: any) => {
12 | e.preventDefault();
13 | const userObj: userObj = {
14 | username: e.target.username.value,
15 | password: e.target.password.value,
16 | };
17 | let data = await fetch('/api/graphql', {
18 | method: 'POST',
19 | headers: {
20 | 'Content-Type': 'application/json',
21 | },
22 | body: JSON.stringify({
23 | query: `mutation {
24 | newUser(username: "${userObj.username}", password: "${userObj.password}") {
25 | token
26 | }
27 | }`,
28 | }),
29 | })
30 | .then(data => {
31 | return data.json();
32 | })
33 | .then(data => {
34 | if (data.data.newUser.token) {
35 | localStorage.setItem('token', data.data.newUser.token);
36 | router.push('/');
37 | } else throw new Error();
38 | });
39 | };
40 |
41 | return (
42 |
43 |
44 |
50 | Register with QuiL
51 |
52 | Registering with QuiL will grant you access to save your URI's and
53 | themes. Simply create your account by entering your desired username
54 | and password or register with you github account.
55 |
56 |
57 |
75 |
110 | OR
111 |
122 |
123 |
127 | Register with Github
128 |
129 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | {
137 | /* Create Account */
138 | }
139 | {
140 | /* Create Account */
141 | }
142 |
143 | export default Register;
144 |
145 | /*
146 | mutation ($password: String, $username: String, $newUserUsername2: String, $newUserPassword2: String) newUser {
147 | newUser ($password: String, $username: String, $newUserUsername2: String, $newUserPassword2: String) {
148 | newUser(password: "${userObj.password}" username: "${userObj.username}") {
149 | token
150 | }
151 | }
152 | */
153 |
--------------------------------------------------------------------------------
/app/(root)/Register/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Register from "./Register";
3 | import { useRouter } from "next/navigation";
4 |
5 | // const handleEmail = () => {
6 | // console.log(e.target.value, ' inside handle email');
7 | // };
8 |
9 | // const handleUsername = () => {
10 | // console.log(e.target.value, ' inside handle username');
11 | // };
12 |
13 | // const handlePassword = () => {
14 | // console.log(e.target.value, ' inside handle password');
15 | // };
16 |
17 | // const handleSubmit = () => {
18 | // console.log(e.target.value, ' handle the submit');
19 | // };
20 |
21 | export default function Page() {
22 | // const router = useRouter();
23 | // router.prefetch(`/Main/Chart?URI=${uriParam}`)
24 |
25 | return (
26 |
27 |
28 |
29 | );
30 | }
--------------------------------------------------------------------------------
/app/(root)/RootContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { useRouter } from 'next/navigation';
4 | import { motion } from 'framer-motion';
5 | import jwt_decode from 'jwt-decode';
6 | import { toast, ToastContainer } from 'react-toastify';
7 | import 'react-toastify/dist/ReactToastify.css';
8 | import { decoded } from './frontendTypes';
9 |
10 | const RootContainer = ({
11 | authCode,
12 | stateCode,
13 | }: {
14 | authCode: string;
15 | stateCode: string;
16 | }) => {
17 | const [initialURI, setInitialURI] = useState(null);
18 | const [sampleURI, setSampleURI] = useState(null);
19 | const [code, setCode] = useState(authCode);
20 | const [stateString, setStateCode] = useState(stateCode);
21 |
22 | const [userJWT, setUserJWT] = useState(null);
23 |
24 | const router = useRouter();
25 |
26 | const handleUserURI = (e: React.ChangeEvent): void => {
27 | let sanitize = e.target.value.trim();
28 | setInitialURI(sanitize);
29 | };
30 | const handleSampleURI = (e: React.ChangeEvent): void => {
31 | setSampleURI(e.target.value);
32 | };
33 | let rootLoading: any;
34 | const sanitizeLaunch = (e: any) => {
35 | if (sampleURI || initialURI.includes('postgres')) {
36 | handleLaunch(e);
37 | rootLoading = toast.loading('Loading content..');
38 | } else {
39 | toast.error('Not a valid PostgreSQL URL');
40 | }
41 | };
42 |
43 | const handleLaunch = (e: React.MouseEvent): void => {
44 | const URI = initialURI ? initialURI : sampleURI;
45 | router.push(`/Main/Chart?URI=${URI}`);
46 | toast.dismiss(rootLoading);
47 | };
48 | useEffect(() => {
49 | const handleLogin = async (code: string) => {
50 | let currJWT = window.localStorage.getItem('token');
51 |
52 | let oauthType;
53 |
54 | if (stateString) {
55 | if (stateString.includes('c2lnbmlu')) oauthType = 'signin';
56 | if (stateString.includes('cmVnaXN0ZXI')) oauthType = 'register';
57 | }
58 |
59 | if (code) {
60 | const queryValue = `mutation {
61 | postOAuth(code: "${code}", oauthType: "${oauthType}") {
62 | token
63 | }
64 | }`;
65 |
66 | const oauthResponse = await fetch('/api/graphql', {
67 | method: 'POST',
68 | headers: {
69 | 'Content-Type': 'application/json',
70 | },
71 | body: JSON.stringify({
72 | query: queryValue,
73 | }),
74 | }).then(res => res.json());
75 |
76 | if (oauthResponse.data.postOAuth.token !== null) {
77 | localStorage.setItem('token', oauthResponse.data.postOAuth.token);
78 | }
79 | }
80 |
81 | currJWT = window.localStorage.getItem('token');
82 | let decoded: decoded;
83 |
84 | if (currJWT || currJWT !== null) {
85 | decoded = jwt_decode(currJWT);
86 | }
87 | // if JWT doesnt exist, set userJWT to null
88 | if (!decoded) setUserJWT(null);
89 | // otherwise decode it and set userJWT object
90 | else setUserJWT(decoded);
91 | };
92 | handleLogin(code);
93 | }, []);
94 |
95 | return (
96 |
97 |
98 | {userJWT ? (
99 |
107 | Welcome back
108 | {userJWT.username}
109 |
110 | ) : (
111 |
112 |
120 | Welcome to
121 |
122 |
131 | QuiL
132 |
133 |
141 | QuiL is a developer tool used to visualize an existing relational
142 | database and generate the GraphQL schemas & resolvers for that
143 | data base. This is intended to help developers see how to
144 | transition to GraphQL from a traditional REST API architecture.
145 |
146 |
147 | )}
148 |
154 |
155 |
156 |
157 | URI
158 |
159 |
167 |
168 |
169 |
170 | Sample Database
171 |
172 |
178 | Pick one
179 | {/*
183 | Star Wars
184 | */}
185 |
186 | Quitr
187 |
188 |
189 |
190 |
191 |
192 |
198 | Launch
199 |
200 |
212 | {userJWT ? (
213 |
214 | {
217 | window.localStorage.removeItem('token');
218 | window.location.reload();
219 | }}
220 | >
221 | Log Out
222 |
223 |
224 | ) : (
225 | <>
226 |
227 |
OR
228 |
229 | router.push('/Login')}
233 | data-cy="root-login-btn"
234 | >
235 | Login
236 |
237 | router.push('/Register')}
240 | data-cy="root-register-btn"
241 | >
242 | Register
243 |
244 |
245 |
246 | >
247 | )}
248 |
249 |
250 |
251 |
252 | );
253 | };
254 |
255 | export default RootContainer;
256 |
--------------------------------------------------------------------------------
/app/(root)/frontendTypes.tsx:
--------------------------------------------------------------------------------
1 | import { type } from 'os';
2 | import { Node, Edge, NodeChange, EdgeChange } from 'reactflow';
3 | import { StringMappingType } from 'typescript';
4 | import {
5 | nodeShape,
6 | ResolverStrings,
7 | SingleSchemaType,
8 | } from '../../server/types';
9 |
10 | export type DisplayContainerProps = {
11 | displayMode: string;
12 | userInputURI: (e: string) => void;
13 | uriLaunch: () => Promise;
14 | resQL: resQL;
15 | schemaGen: () => void;
16 | resolverGen: () => void;
17 | edges: Edge[];
18 | nodes: Node[];
19 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void;
20 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void;
21 | userJWT: any;
22 | userProjects: projectType[] | [];
23 | URI: string;
24 | removeDeletedProject: Function;
25 | };
26 |
27 | export type NavigationBarProps = {
28 | userJWT: object | null;
29 | theme?: string;
30 | handleSetTheme?: (e: string) => void;
31 | aboutPageMode: () => void;
32 | mainPageMode: () => void;
33 | };
34 |
35 | export type VisualizeSchemaResolverProps = {
36 | displayMode: string;
37 | resQL: resQL;
38 | };
39 |
40 | export type SchemaProps = {
41 | resQL: resQL;
42 | };
43 |
44 | export type ResolverProps = {
45 | resQL: resQL;
46 | };
47 |
48 | export type VisualizeDBProps = {
49 | userInputURI: (e: string) => void;
50 | nodes: Node[];
51 | edges: Edge[];
52 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void;
53 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void;
54 | uriLaunch: () => Promise;
55 | };
56 |
57 | export type ChartProps = {
58 | nodes: Node[];
59 | edges: Edge[];
60 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void;
61 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void;
62 | };
63 |
64 | export type AboutPageProps = {
65 | theme: string;
66 | };
67 | // handleSetNodes/handleSetEdges may need to change
68 | export type FlowProps = {
69 | nodes: Node[];
70 | edges: Edge[];
71 | handleSetEdges: (cb: (eds: Edge[]) => Edge[]) => void;
72 | handleSetNodes: (cb: (nds: Node[]) => Node[]) => void;
73 | };
74 |
75 | export type resQL = {
76 | data: { getAllData: getAllData };
77 | };
78 |
79 | export type getAllData = {
80 | nodes: nodeShape[];
81 | resolvers: ResolverStrings[];
82 | schemas: SingleSchemaType[];
83 | };
84 |
85 | export type nodes = node[];
86 |
87 | export type node = {
88 | name: string;
89 | primaryKey: string;
90 | columns: columns;
91 | edges: edge[];
92 | };
93 |
94 | export type edge = {
95 | fKey: string;
96 | refTable: string;
97 | };
98 | export type columns = column[];
99 |
100 | export type column = {
101 | columnName: string;
102 | dataType: string;
103 | };
104 |
105 | export interface data {
106 | name: string;
107 | key: number;
108 | columns: columns;
109 | edges: edge[];
110 | refTables: string[];
111 | arrFKeys: string[];
112 | }
113 |
114 | export type position = {
115 | x: number;
116 | y: number;
117 | };
118 |
119 | export type MainContainerProps = {
120 | URI: string;
121 | initialNodes: Node[];
122 | initialEdges: Edge[];
123 | data: resQL;
124 | };
125 |
126 | export type userObj = {
127 | [k: string]: string;
128 | };
129 |
130 | export type inputObj = {
131 | name: string;
132 | };
133 |
134 | export type loggedUser = {
135 | [k: string]: any;
136 | };
137 |
138 | export type decoded = {
139 | [k: string]: any;
140 | };
141 |
142 | export type projectType = {
143 | name: string;
144 | owner_id: string;
145 | saved_db: string[];
146 | _id: string;
147 | };
148 |
--------------------------------------------------------------------------------
/app/(root)/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import React from "react";
3 | export default function RootLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
15 | // layout page is where you would fetch data (pretty much acts as app.js where app.js is the parent component)
16 | // global components like NavBar or Footer would live here
17 | // layout page can nested. You can have multiple layouts of sub-directories that would only apply to the children components
18 | // about/ example.com/about
19 | // [slug]/ example.com/{slug} --> represents a dynamic route. [slug] acts as a wild card and usually contains things such as ids or username
20 | // (group)/ example.com(???) --> ignores
21 |
--------------------------------------------------------------------------------
/app/(root)/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RootContainer from './RootContainer';
3 |
4 | export default function Page({ searchParams }: any) {
5 | return (
6 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/Main/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/QuiL/8328a039999732b798888c264ee9d47efbe46f90/app/Main/.DS_Store
--------------------------------------------------------------------------------
/app/Main/About/page.tsx:
--------------------------------------------------------------------------------
1 | import NavigationBar from '../Chart/(components)/NavigationBar';
2 |
3 | export default function Page() {
4 |
5 | return (
6 |
7 |
inside /ABOUT
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { motion } from "framer-motion";
3 | import Image from "next/image";
4 | const AboutPage = (): JSX.Element => {
5 | return (
6 |
7 |
8 |
14 |
20 | Nice To Meet You!
21 |
22 |
28 | Here's How You Can Learn More
29 |
30 |
31 |
37 | About Us
38 |
39 |
40 |
41 |
47 |
52 |
59 |
60 |
61 |
62 | Brian Tran
63 |
64 |
Software Engineer
65 |
101 |
102 |
103 |
109 |
114 |
121 |
122 |
123 |
Stephen Fitzsimmons
124 |
Software Engineer
125 |
161 |
162 |
163 |
169 |
174 |
181 |
182 |
183 |
184 | Daniel An
185 |
186 |
Software Engineer
187 |
223 |
224 |
225 |
231 |
236 |
243 |
244 |
245 |
Andres Jaramillo
246 |
Software Engineer
247 |
283 |
284 |
285 |
286 |
287 | );
288 | };
289 |
290 | export default AboutPage;
291 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/Chart.tsx:
--------------------------------------------------------------------------------
1 | import Flow from '../(flow)/Flow';
2 | import React from 'react';
3 | import { ChartProps } from '../../../(root)/frontendTypes';
4 |
5 | const Chart = ({
6 | nodes = null,
7 | edges = null,
8 | handleSetNodes,
9 | handleSetEdges,
10 | }: ChartProps): JSX.Element => {
11 | return (
12 |
13 |
19 |
20 | );
21 | };
22 |
23 | export default Chart;
24 | //
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/DisplayContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import VisualizeDB from './VisualizeDB';
4 | import VisualizeSchemaResolver from './VisualizeSchemaResolver';
5 | import { DisplayContainerProps } from '../../../(root)/frontendTypes';
6 | import { motion } from 'framer-motion';
7 | import SaveContainer from './SaveContainer';
8 | import LoadContainer from './LoadContainer';
9 | import LoadItem from './LoadItem';
10 | import AboutPage from './AboutPage';
11 | import Link from 'next/link';
12 |
13 | const DisplayContainer = ({
14 | displayMode,
15 | userInputURI,
16 | uriLaunch,
17 | resQL,
18 | schemaGen,
19 | resolverGen,
20 | edges,
21 | nodes,
22 | handleSetEdges,
23 | handleSetNodes,
24 | userJWT,
25 | userProjects,
26 | URI,
27 | removeDeletedProject,
28 | }: DisplayContainerProps): JSX.Element => {
29 | const [saveModalVisible, setSaveModalVisible] = useState(true);
30 | const [loadModalVisible, setLoadModalVisible] = useState(true);
31 |
32 | let schemaTabStyle = 'tab tab-bordered';
33 | let resolverTabStyle = 'tab tab-bordered';
34 | switch (displayMode) {
35 | case 'schemaMode':
36 | schemaTabStyle = 'tab tab-bordered tab-active';
37 | break;
38 | case 'resolverMode':
39 | resolverTabStyle = 'tab tab-bordered tab-active';
40 | break;
41 | }
42 |
43 | const saveURIHandler = async (e: any) => {
44 | e.preventDefault();
45 | let data = await fetch('/api/graphql', {
46 | method: 'POST',
47 | headers: {
48 | 'Content-Type': 'application/json',
49 | },
50 | body: JSON.stringify({
51 | query: `mutation {
52 | saveData(projectName: "${e.target.URInickname.value}", projectData: "${e.target.URIstring.value}", userId: ${userJWT.userId}) {
53 | projectId
54 | projectName
55 | success
56 | }
57 | }`,
58 | }),
59 | })
60 | .then((data) => {
61 | return data.json();
62 | })
63 | .then((data) => {
64 | setSaveModalVisible(false);
65 | });
66 | };
67 |
68 | const setLoadVisibility = () => setLoadModalVisible(false);
69 |
70 | const LoadComponents = [];
71 | for (let i = 0; i < userProjects.length; i++) {
72 | LoadComponents.push();
73 | }
74 |
75 | if (displayMode === 'aboutPage') {
76 | return ;
77 | } else
78 | return (
79 |
80 |
81 |
82 |
83 |
84 |
92 | View Schemas/Resolvers
93 |
94 |
100 | userInputURI(e.target.value)}
103 | className="input input-sm input-bordered w-full mx-1"
104 | placeholder="insert URI"
105 | data-cy="insert-uri-main"
106 | >
107 |
108 | uriLaunch()}
112 | data-cy="main-launch-btn"
113 | >
114 | Launch
115 |
116 | {/* Save Button and Modal */}
117 | setSaveModalVisible(true)}
121 | >
122 | Save
123 |
124 |
125 | {saveModalVisible && (
126 | <>
127 |
128 |
133 |
134 |
135 |
139 | ✕
140 |
141 |
142 | {/* MAKE CONDITIONAL*/}
143 | {userJWT ? (
144 | <>
145 |
146 | Save Your Database
147 |
148 |
181 | >
182 | ) : (
183 | <>
184 |
191 | Please login to save your project!!
192 |
193 |
199 |
200 |
206 | Login
207 |
208 |
209 |
210 |
211 | Register
212 |
213 |
214 |
215 | >
216 | )}
217 | {/* MAKE CONDITIONAL*/}
218 |
219 |
220 |
221 | >
222 | )}
223 | {/* Load Button and Modal */}
224 |
225 | setLoadModalVisible(true)}
229 | >
230 | Load
231 |
232 | {loadModalVisible && (
233 |
234 |
239 |
240 |
241 |
245 | ✕
246 |
247 | {userJWT ? (
248 |
249 |
250 |
251 |
252 |
253 | Project Name
254 |
255 |
256 |
257 | {userProjects.map((e: any, i: any) => (
258 |
267 | ))}
268 |
269 |
270 |
271 | ) : (
272 | <>
273 |
280 | Please login to load your project!!
281 |
282 |
288 |
289 |
295 | Login
296 |
297 |
298 |
299 |
300 | Register
301 |
302 |
303 |
304 | >
305 | )}
306 |
307 |
308 |
309 | )}
310 |
311 |
312 |
320 |
321 |
322 |
323 |
324 |
325 | schemaGen()}>
326 | Schemas
327 |
328 | resolverGen()}>
329 | Resolvers
330 |
331 |
332 |
333 |
334 |
335 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 | );
347 | };
348 |
349 | export default DisplayContainer;
350 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/LoadContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function LoadContainer() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | URI Nickname
11 | URI
12 |
13 |
14 |
15 | URI Nickname
16 | URI
17 |
18 |
19 |
20 | URI Nickname
21 | URI
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default LoadContainer;
31 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/LoadItem.tsx:
--------------------------------------------------------------------------------
1 | import { any } from 'cypress/types/bluebird';
2 | import React, { useState } from 'react';
3 | import { projectType } from '../../../(root)/frontendTypes';
4 |
5 | type LoadItemProps = {
6 | userProject: projectType;
7 | key: string;
8 | id: string;
9 | uriLaunch: Function;
10 | setLoadVisibility: Function;
11 | setLoadModalVisible: Function;
12 | removeDeletedProject: Function;
13 | };
14 |
15 | function LoadItem({
16 | userProject,
17 | key,
18 | id,
19 | uriLaunch,
20 | setLoadVisibility,
21 | setLoadModalVisible,
22 | removeDeletedProject,
23 | }: LoadItemProps) {
24 | const [projectVisibility, setProjectVisibility] = useState(true);
25 | const remove = (el: any) => {
26 | let element = el;
27 | element.remove();
28 | };
29 |
30 | const deleteURIHandler = async (id: any): Promise => {
31 | let data = await fetch('/api/graphql', {
32 | method: 'POST',
33 | headers: {
34 | 'Content-Type': 'application/json',
35 | },
36 | body: JSON.stringify({
37 | query: `mutation {
38 | deleteProject(projectId: ${id}) {
39 | deleted
40 | }
41 | }
42 | `,
43 | }),
44 | }).then(data => {
45 | return data.json();
46 | });
47 | setProjectVisibility(false);
48 | };
49 |
50 | const handleLoadClick = (e: any, uri: any) => {
51 | setLoadModalVisible(false);
52 | uriLaunch(e, uri);
53 | };
54 |
55 | return (
56 | <>
57 | {projectVisibility && (
58 |
59 | {key}
60 | {userProject.name}
61 |
62 | handleLoadClick(e, userProject.saved_db)}
66 | >
67 | Load
68 |
69 |
70 |
71 | {
75 | deleteURIHandler(e.target.id);
76 | }}
77 | >
78 | delete
79 |
80 |
81 |
82 | )}
83 | >
84 | );
85 | }
86 |
87 | export default LoadItem;
88 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | // postgres://lkdxllvk:GTIkPygxpPOx0ZVNJ3luQHEfApEIJekP@heffalump.db.elephantsql.com/lkdxllvk
3 | import React, { useEffect, useState } from 'react';
4 | import DisplayContainer from './DisplayContainer';
5 | import { Node, Edge } from 'reactflow';
6 | import createNodes from '../(flow)/Nodes';
7 | import createEdges from '../(flow)/Edges';
8 | import NavigationBar from './NavigationBar';
9 | import jwt_decode from 'jwt-decode';
10 | import { motion } from 'framer-motion';
11 | import { toast, ToastContainer } from 'react-toastify';
12 | import {
13 | MainContainerProps,
14 | projectType,
15 | resQL,
16 | } from '../../../(root)/frontendTypes';
17 |
18 | import 'react-toastify/dist/ReactToastify.css';
19 | const MainContainer = ({
20 | URI,
21 | initialNodes,
22 | initialEdges,
23 | data,
24 | }: MainContainerProps): JSX.Element => {
25 | const [displayMode, setDisplayMode] = useState('schemaMode');
26 | const [uri, setURI] = useState('');
27 | const [resQL, setResQL] = useState(data);
28 | const [nodes, setNodes] = useState(initialNodes);
29 | const [edges, setEdges] = useState(initialEdges);
30 | const [theme, setTheme] = useState('night');
31 | const [userJWT, setUserJWT] = useState();
32 | const [userProjects, setUserProjects] = useState([]);
33 | const [toastTheme, setToastTheme] = useState<'light' | 'dark' | 'colored'>(
34 | 'dark'
35 | );
36 | useEffect(() => {
37 | try {
38 | const getUserProjects = async (): Promise => {
39 | let currJWT = window.localStorage.getItem('token');
40 | let decoded: any;
41 | if (currJWT) {
42 | decoded = await jwt_decode(currJWT);
43 | setUserJWT(decoded);
44 | }
45 | // if JWT doesnt exist, set userJWT to null
46 | if (!decoded) setUserJWT(null);
47 | // otherwise decode it and set userJWT object
48 | if (currJWT) {
49 | let data = await fetch('/api/graphql', {
50 | method: 'POST',
51 | headers: {
52 | 'Content-Type': 'application/json',
53 | },
54 | body: JSON.stringify({
55 | query: `query {
56 | getUserProjects(userId: ${decoded.userId}) {
57 | db {
58 | name
59 | owner_id
60 | saved_db
61 | _id
62 | }
63 | success
64 | }
65 | }`,
66 | }),
67 | })
68 | .then((data) => {
69 | return data.json();
70 | })
71 | .then((data) => {
72 | setUserProjects(data.data.getUserProjects.db);
73 | });
74 | }
75 | };
76 | getUserProjects();
77 | } catch (error) {}
78 | }, []);
79 |
80 | const removeDeletedProject = (id: any) => {
81 | setUserProjects((oldState) => {
82 | return oldState.filter((e: any) => e._id === id);
83 | });
84 | };
85 |
86 | //invoked in VisualizeSchemaResolver
87 | // Schema Mode is to display the Schemas (drawer) generated
88 | const schemaGen = (): void => {
89 | setDisplayMode('schemaMode');
90 | };
91 | //invoked in VisualizeSchemaResolver
92 | // Resolver Mode is to display the Resolvers (drawer) generated
93 | const resolverGen = (): void => {
94 | setDisplayMode('resolverMode');
95 | };
96 |
97 | const aboutPageMode = (): void => {
98 | setDisplayMode('aboutPage');
99 | };
100 |
101 | const mainPageMode = (): void => {
102 | setDisplayMode('mainPage');
103 | };
104 |
105 | //invoked in visualizeDB.
106 | // Checks for error in the users before invoking the fetch
107 | const uriLaunch = async (): Promise => {
108 | // e.preventDefault();
109 | if (uri.includes('postgres')) {
110 | launchUri(uri);
111 | } else {
112 | toast.error('Not a valid PostgreSQL URL');
113 | }
114 | };
115 |
116 | const launchUri = async (loadedUri: string): Promise => {
117 | const toastLoading = toast.loading('loading content');
118 | let launchURI = loadedUri || uri;
119 | let data = await fetch('/api/graphql', {
120 | method: 'POST',
121 | headers: {
122 | 'Content-Type': 'application/json',
123 | },
124 |
125 | body: JSON.stringify({
126 | query: `query GetData {
127 | getAllData(uri: "${launchURI}") {
128 | nodes {
129 | name,
130 | primaryKey,
131 | columns {
132 | columnName,
133 | dataType
134 | },
135 | edges {
136 | fKey,
137 | refTable
138 | }
139 | },
140 | resolvers {
141 | tableName,
142 | resolver
143 | },
144 | schemas {
145 | tableName,
146 | schemas
147 | }
148 | }
149 | }`,
150 | }),
151 | });
152 | let res = await data.json();
153 | toast.dismiss(toastLoading);
154 | if (
155 | res.data.getAllData.nodes.length === 0 &&
156 | res.data.getAllData.resolvers.length === 0 &&
157 | res.data.getAllData.schemas.length === 0
158 | ) {
159 | toast.error('Empty database or bad URL');
160 | }
161 | setResQL(res);
162 | setNodes(createNodes(res));
163 | setEdges(createEdges(res));
164 | };
165 |
166 | // handleSetNodes takes in a callback (cb). That callback takes in
167 | const handleSetNodes = (cb: (nds: Node[]) => Node[]): void => {
168 | setNodes(cb);
169 | };
170 | const handleSetEdges = (): void => {
171 | setEdges(edges);
172 | };
173 |
174 | // invoked inside visualizeDB. users input (uri)
175 | const userInputURI = (e: string): void => {
176 | let sanitize = e.trim();
177 | setURI(sanitize);
178 | };
179 |
180 | // changing the themes for Toast(notifications) and Tailwind/app
181 | const handleSetTheme = (value: any): void => {
182 | setTheme(value);
183 | if (theme !== 'light' && theme !== 'night') {
184 | setToastTheme('colored');
185 | } else setToastTheme('light');
186 | };
187 |
188 | return (
189 |
190 |
195 |
201 |
202 |
214 |
230 |
231 | );
232 | };
233 |
234 | export default MainContainer;
235 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/NavigationBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import { useRouter } from "next/navigation";
4 | import React, { useState } from "react";
5 | import { useSearchParams } from "next/navigation";
6 | import { NavigationBarProps } from "../../../(root)/frontendTypes";
7 | import test from "node:test";
8 | import { setSyntheticLeadingComments } from "typescript";
9 | import quil from "./quil.png";
10 | import { motion } from "framer-motion";
11 | import Image from "next/image";
12 | const NavigationBar = ({
13 | userJWT,
14 | handleSetTheme,
15 | aboutPageMode,
16 | mainPageMode,
17 | }: NavigationBarProps): JSX.Element => {
18 | const router = useRouter();
19 | const searchParams = useSearchParams();
20 | const URIfromRoot = searchParams.get("URI");
21 | const [uriParam, setUriParam] = useState(URIfromRoot);
22 |
23 | return (
24 |
29 |
30 |
31 | mainPageMode()}
34 | >
35 |
36 |
37 |
38 |
39 |
40 | mainPageMode()}
44 | >
45 | Main
46 |
47 | {userJWT ? (
48 | {
52 | window.localStorage.removeItem("token");
53 | window.location.reload();
54 | }}
55 | >
56 | Log Out
57 |
58 | ) : (
59 | <>
60 | {
64 | router.push("/Login");
65 | }}
66 | >
67 | Login
68 |
69 | router.push("/Register")}
73 | >
74 | Register
75 |
76 | >
77 | )}
78 | aboutPageMode()}
82 | >
83 | About
84 |
85 | handleSetTheme(e.target.value)}
88 | className='select bg-neutral-content w-1/3 max-w-xs text-base-300 mr-9'
89 | >
90 |
91 | Theme
92 |
93 | light
94 | night
95 | retro
96 | cyberpunk
97 | synthwave
98 | pastel
99 |
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default NavigationBar;
108 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/Resolver.tsx:
--------------------------------------------------------------------------------
1 | import { ResolverProps } from '../../../(root)/frontendTypes';
2 | import React, { useState } from 'react';
3 | import { Card } from './ResolverDisplay';
4 | import { ResolverStrings } from '../../../../server/types';
5 |
6 | const Resolver = ({ resQL }: ResolverProps): JSX.Element => {
7 | const [copyStatus, setCopyStatus] = useState('Copy');
8 | if (Object.keys(resQL).length === 0) return;
9 | const { resolvers } = resQL.data.getAllData;
10 |
11 | const onClick = () => {
12 | const allResolvers = resolvers.reduce((all, curr) => {
13 | return all + curr.resolver;
14 | }, '');
15 |
16 | const formatted = `Query {` + allResolvers + `\n }`;
17 | navigator.clipboard.writeText(formatted);
18 | setCopyStatus('Copied!');
19 | setTimeout(() => {
20 | setCopyStatus('Copy');
21 | }, 5000);
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | Copy All
30 |
31 |
32 |
33 | {resolvers.map((e: ResolverStrings) => (
34 |
38 | ))}
39 |
40 | );
41 | };
42 |
43 | export default Resolver;
44 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/ResolverDisplay.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { CopyTwoTone } from "@ant-design/icons";
3 | import { javascript } from "@codemirror/lang-javascript";
4 | import { dracula } from "@uiw/codemirror-theme-dracula";
5 | import ReactCodeMirror from "@uiw/react-codemirror";
6 | import React, { useState } from "react";
7 | import { motion } from "framer-motion";
8 |
9 | type CardProps = {
10 | tableName: string;
11 | value: any;
12 | };
13 |
14 | type ResolverMirrorProps = {
15 | value: any;
16 | };
17 |
18 | export const ResolverMirror = ({ value }: ResolverMirrorProps) => {
19 | return (
20 |
21 |
28 |
29 | );
30 | };
31 |
32 | export const Card = ({ value, tableName }: CardProps) => {
33 | const [copyStatus, setCopyStatus] = useState("Copy");
34 |
35 | const onClick = () => {
36 | navigator.clipboard.writeText(value);
37 | setCopyStatus("Copied!");
38 | setTimeout(() => {
39 | setCopyStatus("Copy");
40 | }, 5000);
41 | };
42 | return (
43 |
44 |
45 |
46 |
51 |
52 |
53 | {tableName[0].toUpperCase() +
54 | tableName.substring(1, tableName.length)}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/app/Main/Chart/(components)/SaveContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function SaveContainer() {
4 | return (
5 |
6 | {/* The button to open modal */}
7 |
8 | Save
9 |
10 | {/* Put this part before
{children}
10 |