├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── codeql-analysis.yml │ └── main.yml ├── .gitignore ├── .nvmrc ├── Dockerfile ├── README.md ├── additional.d.ts ├── docker-compose.yml ├── etc └── postgres │ └── 000-initial.sql ├── jest.config.json ├── jest.setup.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── prest.toml ├── public └── favicon.ico ├── src ├── components │ └── Layout.tsx ├── lib │ └── prest.ts ├── pages │ ├── [database] │ │ ├── [scheme] │ │ │ ├── [entity] │ │ │ │ ├── edit │ │ │ │ │ └── [[...id]].tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx └── theme │ └── index.ts ├── tests ├── components │ └── Layout.spec.tsx └── pages │ ├── entity.spec.tsx │ └── scheme.spec.tsx ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["next/babel"] 5 | }, 6 | "production": { 7 | "presets": ["next/babel"] 8 | }, 9 | "test": { 10 | "presets": ["next/babel"], 11 | "plugins": [ 12 | [ 13 | "module-resolver", 14 | { 15 | "root": ["./"], 16 | "alias": { 17 | "~": "./src" 18 | } 19 | } 20 | ] 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | tests 2 | .next 3 | coverage 4 | node_modules 5 | .editorconfig 6 | .git 7 | .gitignore 8 | jest.config.json 9 | jest.setup.tests 10 | README.md 11 | .eslintrc 12 | .nvmrc 13 | Dockerfile 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "rules": { 12 | "prettier/prettier": [ 13 | "error", {"endOfLine": "auto"} 14 | ], 15 | "indent": 0, 16 | "comma-dangle": ["error", "always-multiline"] 17 | }, 18 | "overrides": [ 19 | { 20 | "files": ["tests/e2e/**/*.ts"], 21 | "rules": { 22 | "@typescript-eslint/no-explicit-any": 0, 23 | "@typescript-eslint/no-var-requires": 0 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 8 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: ⏳ Install Deeps 14 | run: yarn install 15 | 16 | - name: 🔎 Verify lint 17 | run: yarn lint 18 | 19 | - name: 🩺 Unity Test 20 | run: yarn test 21 | 22 | docs: 23 | if: github.event_name != 'pull_request' 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: 🔨 Build Docs 29 | run: | 30 | cd docs 31 | yarn install 32 | yarn build 33 | 34 | - name: Deploy 🚀 35 | uses: JamesIves/github-pages-deploy-action@4.1.4 36 | with: 37 | branch: gh-pages 38 | folder: ./docs/build 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | data/ 32 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.17.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | ENV NODE_ENV "production" 6 | EXPOSE 3001 7 | 8 | RUN yarn install 9 | 10 | CMD ["yarn", "dev", "-p", "3001"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build UI - prestd server (PostgreSQL ➕ REST) 2 | 3 | Build UI, is **"Django Admin"** _like_ created in React to support prestd interactions. 4 | 5 | ### How use it 6 | 7 | We use (and recommend) **node version 14**, to avoid messing up your environment it is recommended to use a name version control, e.g. [`nvm`](https://github.com/nvm-sh/nvm): 8 | 9 | ```sh 10 | nvm install $(cat .nvmrc) # or nvm use $(cat .nvmrc) 11 | ``` 12 | 13 | `buildui` depends on some services (postgresql and prestd server), to simplify the creation of your environment we recommend to use **docker**, to make it even easier we wrote a _[docker compose](https://docs.docker.com/compose/)_ (contained here in the repository): 14 | 15 | ```sh 16 | docker-compose up -d postgres prestd 17 | ``` 18 | 19 | To install the libraries on your node (we use [`yarn`](https://yarnpkg.com/)): 20 | 21 | ```sh 22 | yarn install 23 | yarn dev -p 3001 # the default next port (3000) we use in prestd 24 | ``` 25 | 26 | If you want to set the prestd address use the `PREST_URL` environment variable: 27 | 28 | ```sh 29 | PREST_URL= yarn dev -p 3001 30 | ``` 31 | 32 | > ``: if you ran **prestd** via docker the url will be `http://127.0.0.1:3000` 33 | 34 | ### How use Docker 35 | 36 | > buildui is under development, we have not yet made a docker image available 37 | 38 | **soon docker image:** 39 | 40 | ```sh 41 | docker pull ghcr.io/prest/buildui 42 | docker run -it -e PREST_URL= -p 3001:3001 ghcr.io/prest/buildui 43 | ``` 44 | 45 | ## Issues 46 | 47 | > The [issue listing](https://github.com/prest/prest/issues?q=is%3Aissue+is%3Aopen+label%3Aproduct%2Fadmin) should be kept in the ["main" repository (_api server_)](https://github.com/prest/prest), centralizing all demands helps us give visibility to all products 48 | -------------------------------------------------------------------------------- /additional.d.ts: -------------------------------------------------------------------------------- 1 | declare type Any = string | number | date | boolean; 2 | declare type AnyArray = string[] | number[] | date[] | boolean[]; 3 | declare type AnyObject = Record< 4 | string | number, 5 | Any | AnyArray | AnyObject | AnyObject[] 6 | >; 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | image: postgres 5 | volumes: 6 | - "./data/postgres:/var/lib/postgresql/data" 7 | - "./etc/postgres:/docker-entrypoint-initdb.d" 8 | environment: 9 | - POSTGRES_USER=prest 10 | - POSTGRES_DB=prest 11 | - POSTGRES_PASSWORD=prest 12 | ports: 13 | - "5432:5432" 14 | healthcheck: 15 | test: ["CMD-SHELL", "pg_isready", "-U", "prest"] 16 | interval: 30s 17 | retries: 3 18 | prestd: 19 | # use latest build - analyze the risk of using this version in production 20 | image: ghcr.io/prest/prest:beta 21 | links: 22 | - "postgres:postgres" 23 | environment: 24 | - PREST_DEBUG=false 25 | - PREST_AUTH_ENABLED=false 26 | - PREST_PG_HOST=postgres 27 | - PREST_PG_USER=prest 28 | - PREST_PG_PASS=prest 29 | - PREST_PG_DATABASE=prest 30 | - PREST_PG_PORT=5432 31 | - PREST_SSL_MODE=disable 32 | volumes: 33 | - "./prest.toml:/app/prest.toml" 34 | depends_on: 35 | postgres: 36 | condition: service_healthy 37 | ports: 38 | - "3000:3000" 39 | 40 | # buildui: 41 | # build: . 42 | # links: 43 | # - "prestd:prestd" 44 | # environment: 45 | # - PREST_URL=http://prestd:3000 46 | # - PREST_DATABASE=prest 47 | # - PREST_SCHEME=public 48 | # depends_on: 49 | # postgres: 50 | # condition: service_healthy 51 | # ports: 52 | # - "3001:3001" 53 | -------------------------------------------------------------------------------- /etc/postgres/000-initial.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS example_table ( 2 | id serial, 3 | field_char_50_unique VARCHAR ( 50 ) UNIQUE NOT NULL, 4 | email VARCHAR ( 255 ) UNIQUE NOT NULL, 5 | field_text TEXT, 6 | field_type_jsonb jsonb, 7 | created_at timestamp without time zone NOT NULL DEFAULT now(), 8 | PRIMARY KEY (id) 9 | ); 10 | 11 | INSERT INTO example_table 12 | (field_char_50_unique, email, field_text, field_type_jsonb) 13 | VALUES 14 | ('unique 50 - 01', 'opensource@prestd.com', 'text field', '{"field1": "value field 1", "field2": "value field 2", "int_field": 10}'), 15 | ('unique 50 - 02', 'open-source@prestd.com', 'text field', '{"field1": "value field 1", "field2": "value field 2", "int_field": 11}'), 16 | ('unique 50 - 03', 'buildui@prestd.com', 'text field', '{"field1": "value field 1", "field2": "value field 2", "int_field": 12}'); 17 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "jsdom", 3 | "transform": { 4 | "\\.[jt]sx?$": "babel-jest" 5 | }, 6 | "setupFilesAfterEnv": ["/jest.setup.ts"], 7 | "testPathIgnorePatterns": ["/.next/", "/node_modules/"], 8 | "coverageReporters": ["html"] 9 | } 10 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | PREST_URL: process.env.PREST_URL || "http://127.0.0.1:3000", 4 | PORT: 3001, 5 | }, 6 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prestd-buildui", 3 | "version": "0.2.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint './src'", 10 | "test": "NODE_ENV=test jest -c jest.config.json" 11 | }, 12 | "husky": { 13 | "hooks": { 14 | "pre-commit": "pretty-quick --staged && lint-staged", 15 | "pre-push": "yarn tsc" 16 | } 17 | }, 18 | "lint-staged": { 19 | "*.{ts}": [ 20 | "yarn lint -- --fix" 21 | ] 22 | }, 23 | "dependencies": { 24 | "@fortawesome/fontawesome-svg-core": "^6.1.1", 25 | "@fortawesome/free-solid-svg-icons": "^6.1.1", 26 | "@fortawesome/react-fontawesome": "^0.1.18", 27 | "@material-ui/core": "^4.12.4", 28 | "@material-ui/data-grid": "^4.0.0-alpha.37", 29 | "@material-ui/icons": "^4.11.3", 30 | "@postgresrest/node": "^0.2.0", 31 | "@types/styled-components": "^5.1.25", 32 | "next": "12.1.5", 33 | "node-fetch": "3.2.3", 34 | "react": "18.0.0", 35 | "react-dom": "18.0.0", 36 | "styled-components": "^5.3.5" 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^5.14.1", 40 | "@testing-library/react": "^13.1.0", 41 | "@types/node": "^17.0.12", 42 | "@types/react": "^18.0.5", 43 | "@typescript-eslint/eslint-plugin": "^5.10.1", 44 | "@typescript-eslint/parser": "^5.10.1", 45 | "babel-jest": "^27.1.1", 46 | "babel-plugin-module-resolver": "^4.1.0", 47 | "eslint": "^8.7.0", 48 | "eslint-config-next": "^12.0.8", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "jest": "^27.1.1", 52 | "lint-staged": "^12.3.8", 53 | "prettier": "^2.4.0", 54 | "pretty-quick": "^3.1.3", 55 | "typescript": "^4.6.3" 56 | } 57 | } -------------------------------------------------------------------------------- /prest.toml: -------------------------------------------------------------------------------- 1 | [auth] 2 | enabled = false 3 | table = "prest_users" 4 | username = "username" 5 | password = "password" 6 | metadata = ["first_name", "last_name", "last_login"] 7 | 8 | [jwt] 9 | default = false 10 | 11 | [http] 12 | port = 3000 13 | 14 | [cache] 15 | enabled = false 16 | 17 | [cors] 18 | alloworigin = ["http://127.0.0.1", "http://127.0.0.1:3000", "http://127.0.0.1:3001"] 19 | allowheaders = ["GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"] 20 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prest/prestd-buildui/f78ba8e5d2217dfda6b13099f84c5e2e5b22f9d9/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from "@material-ui/core/AppBar"; 2 | import { 3 | createStyles, 4 | Theme, 5 | withStyles, 6 | WithStyles, 7 | } from "@material-ui/core/styles"; 8 | import Toolbar from "@material-ui/core/Toolbar"; 9 | import Link from "next/link"; 10 | import React from "react"; 11 | import styled from "styled-components"; 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | root: { 16 | position: "fixed", 17 | width: "100%", 18 | bottom: 0, 19 | top: theme.mixins.toolbar.minHeight, 20 | 21 | "@media (min-width:0px) and (orientation: landscape)": { 22 | top: ( 23 | theme.mixins.toolbar[ 24 | "@media (min-width:0px) and (orientation: landscape)" 25 | ] as { minHeight: number } 26 | ).minHeight, 27 | }, 28 | 29 | "@media (min-width:600px)": { 30 | top: ( 31 | theme.mixins.toolbar["@media (min-width:600px)"] as { 32 | minHeight: number; 33 | } 34 | ).minHeight, 35 | }, 36 | }, 37 | }); 38 | 39 | const WhiteLink = styled.a` 40 | color: white; 41 | `; 42 | 43 | export const Layout: React.FC> = ({ 44 | children, 45 | classes, 46 | }) => ( 47 | <> 48 | 49 | 50 | 51 | Build UI - prestd 52 | 53 | 54 | 55 |
{children}
56 | 57 | ); 58 | 59 | export default withStyles(styles)(Layout); 60 | -------------------------------------------------------------------------------- /src/lib/prest.ts: -------------------------------------------------------------------------------- 1 | import PRestAPI from "@postgresrest/node"; 2 | 3 | export default new PRestAPI({ 4 | baseUrl: process.env.PREST_URL, 5 | }); 6 | -------------------------------------------------------------------------------- /src/pages/[database]/[scheme]/[entity]/edit/[[...id]].tsx: -------------------------------------------------------------------------------- 1 | import Box from "@material-ui/core/Box"; 2 | import Button from "@material-ui/core/Button"; 3 | import TextField from "@material-ui/core/TextField"; 4 | import { PRestQuery, PRestTableShowItem } from "@postgresrest/node"; 5 | import { GetServerSideProps } from "next"; 6 | import { useRouter } from "next/router"; 7 | import React, { useState } from "react"; 8 | import Layout from "~/components/Layout"; 9 | import prest from "~/lib/prest"; 10 | 11 | export type Props = { 12 | structures: PRestTableShowItem[]; 13 | data: AnyObject; 14 | fullTableName: string; 15 | tableURL: string; 16 | }; 17 | 18 | export const Home: React.FC = ({ 19 | structures, 20 | data, 21 | fullTableName, 22 | tableURL, 23 | }) => { 24 | const router = useRouter(); 25 | const [form, setForm] = useState( 26 | structures.reduce( 27 | (acc, cur) => ({ 28 | ...acc, 29 | [cur.column_name]: data[cur.column_name], 30 | }), 31 | {} 32 | ) 33 | ); 34 | 35 | const onClick = async () => { 36 | try { 37 | const query = new PRestQuery(); 38 | const table = prest.tableConnection(`${fullTableName}`); 39 | 40 | if (!data.id) { 41 | await table.create(form); 42 | } else { 43 | const { id, ...formWithoutID } = form; 44 | await table.update(query.eq("id", id), formWithoutID); 45 | } 46 | 47 | router.push(`${tableURL}`); 48 | } catch (e) { 49 | console.log(e); 50 | } 51 | }; 52 | 53 | return ( 54 | 55 | 63 | {structures.map(({ column_name, default_value }) => ( 64 | 65 | -1} 69 | label={column_name} 70 | value={form[column_name]} 71 | onChange={(e) => 72 | setForm({ ...form, [column_name]: e.target.value }) 73 | } 74 | /> 75 | 76 | ))} 77 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 86 | const fullTableName = `${ctx.params.database}.${ctx.params.scheme}.${ctx.params.entity}`; 87 | const tableURL = `/${ctx.params.database}/${ctx.params.scheme}/${ctx.params.entity}`; 88 | const { id = ctx.params.id } = []; 89 | const props = { 90 | data: {}, 91 | structures: [], 92 | fullTableName: fullTableName, 93 | tableURL: tableURL, 94 | }; 95 | 96 | const promises = [prest.show(`${fullTableName}`)]; 97 | if (id?.length > 0) { 98 | const query = new PRestQuery(); 99 | promises.push( 100 | prest.tableConnection(`${fullTableName}`).query(query.eq("id", id)) 101 | ); 102 | } 103 | const [structureGet, dataGet] = await Promise.allSettled(promises); 104 | 105 | if (structureGet.status === "fulfilled") { 106 | props.structures = await ( 107 | structureGet as PromiseFulfilledResult 108 | ).value; 109 | 110 | if (dataGet && dataGet.status === "fulfilled") { 111 | props.data = await (dataGet as PromiseFulfilledResult) 112 | .value[0]; 113 | } 114 | } 115 | 116 | return { props }; 117 | }; 118 | 119 | export default Home; 120 | -------------------------------------------------------------------------------- /src/pages/[database]/[scheme]/[entity]/index.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@material-ui/core"; 2 | import Fab from "@material-ui/core/Fab"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import { DataGrid, GridColDef } from "@material-ui/data-grid"; 5 | import AddIcon from "@material-ui/icons/Add"; 6 | import RemoveIcon from "@material-ui/icons/Delete"; 7 | import { PRestQuery, PRestTableShowItem } from "@postgresrest/node"; 8 | import { GetServerSideProps } from "next"; 9 | import { useRouter } from "next/router"; 10 | import React, { useState } from "react"; 11 | import Layout from "~/components/Layout"; 12 | import prest from "~/lib/prest"; 13 | 14 | export type Props = { 15 | items: AnyObject[]; 16 | structure: PRestTableShowItem[]; 17 | fullTableName: string; 18 | }; 19 | 20 | const useStyles = makeStyles((theme) => ({ 21 | fab: { 22 | position: "fixed", 23 | bottom: theme.spacing(2), 24 | right: theme.spacing(2), 25 | 26 | "& > *": { 27 | marginLeft: theme.spacing(2), 28 | }, 29 | }, 30 | table: { 31 | width: "100%", 32 | height: `calc(100vh - ${ 33 | (theme.mixins.toolbar.minHeight as number) + 80 34 | }px)`, 35 | padding: theme.spacing(2), 36 | 37 | "@media (min-width:0px) and (orientation: landscape)": { 38 | height: `calc(100vh - ${ 39 | (( 40 | theme.mixins.toolbar[ 41 | "@media (min-width:0px) and (orientation: landscape)" 42 | ] as typeof theme.mixins.toolbar 43 | ).minHeight as number) + 80 44 | }px`, 45 | }, 46 | 47 | "@media (min-width:600px)": { 48 | height: `calc(100vh - ${ 49 | (( 50 | theme.mixins.toolbar[ 51 | "@media (min-width:600px)" 52 | ] as typeof theme.mixins.toolbar 53 | ).minHeight as number) + 80 54 | }px`, 55 | }, 56 | }, 57 | })); 58 | 59 | export const EntityPage: React.FC = ({ 60 | items, 61 | structure, 62 | fullTableName, 63 | }) => { 64 | const [selectionModel, setSelectionModel] = useState([]); 65 | const classes = useStyles(); 66 | const router = useRouter(); 67 | const columns: GridColDef[] = structure.map(({ column_name }) => ({ 68 | field: column_name, 69 | flex: 1, 70 | })); 71 | 72 | const handleSelectionModelChange = (newSelection) => 73 | setSelectionModel(newSelection.selectionModel); 74 | 75 | const deleteRows = async () => { 76 | const query = new PRestQuery(); 77 | await prest 78 | .tableConnection(`${fullTableName}`) 79 | .delete(query.in("id", selectionModel)); 80 | }; 81 | 82 | return ( 83 | 84 |
85 | router.push(`${router.asPath}/edit/${id}`)} 94 | /> 95 |
96 |
97 | {selectionModel?.length === 0 ? null : ( 98 | 99 | 100 | 101 | 102 | 103 | )} 104 | router.push(`${router.asPath}/edit`)} 108 | > 109 | 110 | 111 |
112 |
113 | ); 114 | }; 115 | 116 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 117 | const fullTableName = `${ctx.params.database}.${ctx.params.scheme}.${ctx.params.entity}`; 118 | const props = { 119 | fullTableName: fullTableName, 120 | items: [], 121 | structure: [], 122 | }; 123 | const [entityGet, structureGet] = await Promise.allSettled([ 124 | prest.tablesByDBInSchema(fullTableName), 125 | prest.show(fullTableName), 126 | ]); 127 | 128 | if (entityGet.status === "fulfilled" && structureGet.status === "fulfilled") { 129 | props.items = (entityGet as PromiseFulfilledResult).value; 130 | props.structure = (structureGet as PromiseFulfilledResult).value; 131 | } 132 | 133 | return { props }; 134 | }; 135 | 136 | export default EntityPage; 137 | -------------------------------------------------------------------------------- /src/pages/[database]/[scheme]/index.tsx: -------------------------------------------------------------------------------- 1 | import { library } from "@fortawesome/fontawesome-svg-core"; 2 | import { faTable } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import List from "@material-ui/core/List"; 5 | import ListItem from "@material-ui/core/ListItem"; 6 | import { PRestTable } from "@postgresrest/node"; 7 | import { GetServerSideProps } from "next"; 8 | import Link from "next/link"; 9 | import { useRouter } from "next/router"; 10 | import React from "react"; 11 | import Layout from "~/components/Layout"; 12 | import prest from "~/lib/prest"; 13 | 14 | export type Props = { 15 | tables: PRestTable[]; 16 | }; 17 | library.add(faTable); 18 | export const SchemePage: React.FC = ({ tables = [] }) => { 19 | const router = useRouter(); 20 | return ( 21 | 22 | 23 | {tables.length > 0 ? ( 24 | tables.map(({ name }) => ( 25 | 30 | 31 | {name} 32 | 33 | 34 | )) 35 | ) : ( 36 | Do not found any Schemes 37 | )} 38 | 39 | 40 | ); 41 | }; 42 | 43 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 44 | const fullSchemeName = `${ctx.params?.database}.${ctx.params?.scheme}`; 45 | const tables = await prest.tablesByDBInSchema(fullSchemeName); 46 | return { props: { tables } }; 47 | }; 48 | 49 | export default SchemePage; 50 | -------------------------------------------------------------------------------- /src/pages/[database]/index.tsx: -------------------------------------------------------------------------------- 1 | import { library } from "@fortawesome/fontawesome-svg-core"; 2 | import { faLayerGroup } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import List from "@material-ui/core/List"; 5 | import ListItem from "@material-ui/core/ListItem"; 6 | import { PRestSchema } from "@postgresrest/node"; 7 | import { GetServerSideProps } from "next"; 8 | import Link from "next/link"; 9 | import React from "react"; 10 | import Layout from "~/components/Layout"; 11 | import prest from "~/lib/prest"; 12 | 13 | export type Props = { 14 | database: string; 15 | schemes: PRestSchema[]; 16 | }; 17 | 18 | library.add(faLayerGroup); 19 | export const DataDasePage: React.FC = ({ database, schemes }) => { 20 | return ( 21 | 22 | 23 | {schemes.length > 0 ? ( 24 | schemes.map(({ schema_name }) => ( 25 | 30 | 31 | {database}. 32 | {schema_name} 33 | 34 | 35 | )) 36 | ) : ( 37 | Do not found any Database 38 | )} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 45 | const props = { 46 | database: ctx.params.database as string, 47 | schemes: await prest.schemas(), 48 | }; 49 | 50 | return { props }; 51 | }; 52 | 53 | export default DataDasePage; 54 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from "@material-ui/core/CssBaseline"; 2 | import { ThemeProvider } from "@material-ui/core/styles"; 3 | import { AppType } from "next/dist/shared/lib/utils"; 4 | import Head from "next/head"; 5 | import React from "react"; 6 | import theme from "~/theme"; 7 | 8 | const App: AppType = (props) => { 9 | const { Component, pageProps } = props; 10 | 11 | React.useEffect(() => { 12 | const jssStyles = document.querySelector("#jss-server-side"); 13 | if (jssStyles) { 14 | jssStyles.parentElement.removeChild(jssStyles); 15 | } 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | Build UI - prestd 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | DocumentContext, 8 | } from "next/document"; 9 | import { ServerStyleSheets } from "@material-ui/core/styles"; 10 | 11 | import theme from "~/theme"; 12 | 13 | interface InitialProps { 14 | styles: Record[]; 15 | html: string; 16 | head?: React.ReactElement[]; 17 | } 18 | 19 | export class CustomDocument extends Document { 20 | static async getInitialProps(ctx: DocumentContext): Promise { 21 | const sheets = new ServerStyleSheets(); 22 | const originalRenderPage = ctx.renderPage; 23 | 24 | ctx.renderPage = () => 25 | originalRenderPage({ 26 | enhanceApp: (App) => (props) => sheets.collect(), 27 | }); 28 | 29 | const initialProps = await Document.getInitialProps(ctx); 30 | 31 | return { 32 | ...initialProps, 33 | // Styles fragment is rendered after the app and page rendering finish. 34 | styles: [ 35 | ...React.Children.toArray(initialProps.styles), 36 | sheets.getStyleElement(), 37 | ], 38 | }; 39 | } 40 | 41 | render(): React.ReactElement { 42 | return ( 43 | 44 | 45 | 46 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default CustomDocument; 61 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { library } from "@fortawesome/fontawesome-svg-core"; 2 | import { faDatabase } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import List from "@material-ui/core/List"; 5 | import ListItem from "@material-ui/core/ListItem"; 6 | import { PRestDatabase } from "@postgresrest/node"; 7 | import { GetServerSideProps } from "next"; 8 | import Link from "next/link"; 9 | import React from "react"; 10 | import Layout from "~/components/Layout"; 11 | import prest from "~/lib/prest"; 12 | 13 | export type Props = { 14 | dbs: PRestDatabase[]; 15 | }; 16 | 17 | library.add(faDatabase); 18 | export const Databases: React.FC = ({ dbs = [] }) => ( 19 | 20 | 21 | {dbs.length > 0 ? ( 22 | dbs.map(({ datname }) => ( 23 | 24 | 25 | {datname} 26 | 27 | 28 | )) 29 | ) : ( 30 | Do not found any Database 31 | )} 32 | 33 | 34 | ); 35 | 36 | export const getServerSideProps: GetServerSideProps = async () => { 37 | const dbs = await prest.databases(); 38 | return { props: { dbs } }; 39 | }; 40 | 41 | export default Databases; 42 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@material-ui/core/styles"; 2 | 3 | export default createTheme({}); 4 | -------------------------------------------------------------------------------- /tests/components/Layout.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | 4 | import Layout from "~/components/Layout"; 5 | 6 | describe("components/Layout", () => { 7 | it("should render component with props", () => { 8 | const children = "my test"; 9 | render(); 10 | 11 | expect(screen.getByText(children)).toHaveClass("Layout-root-1"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/pages/entity.spec.tsx: -------------------------------------------------------------------------------- 1 | jest.mock("~/lib/prest"); 2 | 3 | import { PRestTableShowItem } from "@postgresrest/node"; 4 | import { render, screen } from "@testing-library/react"; 5 | import React from "react"; 6 | import prest from "~/lib/prest"; 7 | import { 8 | EntityPage, 9 | getServerSideProps, 10 | Props 11 | } from "~/pages/[database]/[scheme]/[entity]"; 12 | 13 | describe("components/EntityPage", () => { 14 | it("should render component with props", () => { 15 | const entity = "fakeEnt"; 16 | const stucture = [ 17 | { column_name: "id" }, 18 | { column_name: "col1" }, 19 | ] as PRestTableShowItem[]; 20 | 21 | const items = [ 22 | { id: 1, col1: "foo" }, 23 | { id: 2, col1: "fizz" }, 24 | ]; 25 | 26 | const fullTableName = `prest.public.${entity}`; 27 | 28 | render( 29 | 34 | ); 35 | 36 | items.forEach(({ col1, id }) => { 37 | expect(screen.getByText(id)).toHaveClass( 38 | "MuiDataGrid-cell MuiDataGrid-cell--textLeft" 39 | ); 40 | expect(screen.getByText(col1)).toHaveClass( 41 | "MuiDataGrid-cell MuiDataGrid-cell--textLeft" 42 | ); 43 | }); 44 | }); 45 | 46 | it("should exec correctly getServerSideProps", async () => { 47 | const fakeTables = "fakeTables"; 48 | const fakeStructure = "fakeStructure"; 49 | const fakeCtx = { 50 | params: { 51 | database: "prest", 52 | scheme: "public", 53 | entity: "foobar", 54 | }, 55 | }; 56 | (prest.tablesByDBInSchema as jest.Mock).mockResolvedValue(fakeTables); 57 | (prest.show as jest.Mock).mockResolvedValue(fakeStructure); 58 | 59 | const { props } = (await getServerSideProps(fakeCtx as Any)) as { 60 | props: Props; 61 | }; 62 | 63 | const fullTableName = `${fakeCtx.params.database}.${fakeCtx.params.scheme}.${fakeCtx.params.entity}`; 64 | expect(props).toHaveProperty("fullTableName", fullTableName); 65 | expect(props).toHaveProperty("structure", fakeStructure); 66 | expect(props).toHaveProperty("items", fakeTables); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/pages/scheme.spec.tsx: -------------------------------------------------------------------------------- 1 | jest.mock("~/lib/prest"); 2 | 3 | import { render, screen } from "@testing-library/react"; 4 | import React from "react"; 5 | import prest from "~/lib/prest"; 6 | import { 7 | getServerSideProps, 8 | Props, 9 | SchemePage 10 | } from "~/pages/[database]/[scheme]"; 11 | 12 | describe("components/scheme", () => { 13 | it("should render component with props", () => { 14 | const table1 = { name: "table1" }; 15 | const table2 = { name: "table2" }; 16 | const listItemClass = 17 | "MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"; 18 | 19 | render(); 20 | 21 | const t1El = screen.getByText(table1.name); 22 | expect(t1El).toHaveProperty("href", `http://localhost/${table1.name}`); 23 | expect(t1El).toHaveClass(listItemClass); 24 | 25 | const t2El = screen.getByText(table2.name); 26 | expect(t2El).toHaveProperty("href", `http://localhost/${table2.name}`); 27 | expect(t2El).toHaveClass(listItemClass); 28 | }); 29 | 30 | it("should exec correctly getServerSideProps", async () => { 31 | const fakeTables = "fakeTables"; 32 | (prest.tablesByDBInSchema as jest.Mock).mockResolvedValue(fakeTables); 33 | 34 | const { props } = (await getServerSideProps({} as Any)) as { props: Props }; 35 | expect(props).toHaveProperty("tables", fakeTables); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "strictNullChecks": true, 20 | "removeComments": true, 21 | "noImplicitAny": true, 22 | "jsx": "preserve", 23 | "baseUrl": ".", 24 | "paths": { 25 | "~/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "incremental": true 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "additional.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------