Compare commits
238 Commits
@nhost/nex
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c6f1e3b33 | ||
|
|
d1365ea516 | ||
|
|
72dbba7881 | ||
|
|
a3f3991d5a | ||
|
|
c71fe2cf72 | ||
|
|
24c5ed3ea4 | ||
|
|
2d9145f918 | ||
|
|
9a0ab5b887 | ||
|
|
1ddf704c5b | ||
|
|
6f4ee845c6 | ||
|
|
76ce7d7b6e | ||
|
|
538bfbcb3e | ||
|
|
07b35d1e5f | ||
|
|
2200a0ed07 | ||
|
|
df23d97126 | ||
|
|
104f149369 | ||
|
|
01228583a0 | ||
|
|
93309dd851 | ||
|
|
2cc18dcb51 | ||
|
|
3b48a62790 | ||
|
|
8897dec056 | ||
|
|
324dda8309 | ||
|
|
95f62bed07 | ||
|
|
0e4d8ff118 | ||
|
|
baec5bada7 | ||
|
|
4e56cfc628 | ||
|
|
54bc91923f | ||
|
|
77b12feb95 | ||
|
|
32d4670bbb | ||
|
|
1dc09942d2 | ||
|
|
3343a36358 | ||
|
|
b755e9086c | ||
|
|
48866d0ee1 | ||
|
|
5b3b76bd41 | ||
|
|
7f7e7ea7d4 | ||
|
|
aaaf2dc9c5 | ||
|
|
fa9c1ea28c | ||
|
|
87eda76e2b | ||
|
|
8a596f2a9e | ||
|
|
89b70eb93c | ||
|
|
d6d2381598 | ||
|
|
860d872d07 | ||
|
|
b9917c0c69 | ||
|
|
bf1e4071db | ||
|
|
e5601581f5 | ||
|
|
5013213bc3 | ||
|
|
8be094be54 | ||
|
|
43e5221119 | ||
|
|
6f8feaffc5 | ||
|
|
284ef7e7f2 | ||
|
|
6d5c202da9 | ||
|
|
cf347341d4 | ||
|
|
5262fac6d5 | ||
|
|
d6c670a78b | ||
|
|
197e406209 | ||
|
|
6f0ac5706c | ||
|
|
5fc78964dc | ||
|
|
1bb71df7a1 | ||
|
|
5880f0cd17 | ||
|
|
d7404e2247 | ||
|
|
2aa91cf266 | ||
|
|
b2b17d71aa | ||
|
|
0785a41070 | ||
|
|
c6c8569088 | ||
|
|
c38f60740e | ||
|
|
9342937440 | ||
|
|
e89cd4e262 | ||
|
|
a05438352b | ||
|
|
78437959bb | ||
|
|
e1a7433adb | ||
|
|
a37a430b9f | ||
|
|
7b970e6858 | ||
|
|
5bdb2c605d | ||
|
|
a08cfc696d | ||
|
|
9c4223c547 | ||
|
|
7de3ed5323 | ||
|
|
83f4e32972 | ||
|
|
7b86f38ba3 | ||
|
|
a689b0dbe0 | ||
|
|
14235d08bc | ||
|
|
e0a987248f | ||
|
|
1303fc60ac | ||
|
|
172cab87c7 | ||
|
|
a463d5c3bf | ||
|
|
041385f19a | ||
|
|
47949ed4af | ||
|
|
6a52a267ff | ||
|
|
f33242f2c4 | ||
|
|
b9013e2f6e | ||
|
|
bbe6fb39c8 | ||
|
|
955e3e1316 | ||
|
|
2dba783ad3 | ||
|
|
e23cf74975 | ||
|
|
a3d01c4fad | ||
|
|
4cdcef9ef5 | ||
|
|
df894ef7e2 | ||
|
|
dbc2fadd87 | ||
|
|
f7dd6a9fc6 | ||
|
|
2949ff0f62 | ||
|
|
4578065807 | ||
|
|
feb0c20eb3 | ||
|
|
e3efed65a4 | ||
|
|
194dec1f00 | ||
|
|
0a1b36e600 | ||
|
|
902f486bbe | ||
|
|
fa544cc696 | ||
|
|
7f959b3544 | ||
|
|
bf5056f14b | ||
|
|
b3e674ced0 | ||
|
|
1f9720fa25 | ||
|
|
747bc1104a | ||
|
|
887c168b1b | ||
|
|
038d903555 | ||
|
|
deb14b510b | ||
|
|
8ee23c9303 | ||
|
|
bca1835ecd | ||
|
|
1527b0a455 | ||
|
|
375e53a3f0 | ||
|
|
96e3ca5a32 | ||
|
|
0e570df9c5 | ||
|
|
1f4c67283e | ||
|
|
fc1c4861a3 | ||
|
|
eb55408f85 | ||
|
|
74feaf6add | ||
|
|
e9c8909c6b | ||
|
|
d539a103d9 | ||
|
|
8cd97206cc | ||
|
|
02197639f2 | ||
|
|
38b594aef9 | ||
|
|
f3a8886cd0 | ||
|
|
8d76cf8d40 | ||
|
|
3e1fb974e4 | ||
|
|
f74871d872 | ||
|
|
3f26056688 | ||
|
|
1bc3c30f85 | ||
|
|
4be6406a1e | ||
|
|
88a992ba36 | ||
|
|
6a7801be93 | ||
|
|
7bc5bb857c | ||
|
|
c957039d75 | ||
|
|
96c4032424 | ||
|
|
ec70126b56 | ||
|
|
86b9f9040c | ||
|
|
222f03725b | ||
|
|
10b786e5c6 | ||
|
|
aa8ae88d12 | ||
|
|
0f2c86b41a | ||
|
|
a4c76892dd | ||
|
|
00d278b2cc | ||
|
|
cb6b5faeb9 | ||
|
|
7c4c847b91 | ||
|
|
908887d8c5 | ||
|
|
a2d67bc2db | ||
|
|
1a6cd78254 | ||
|
|
6500629c4b | ||
|
|
add3c2c10e | ||
|
|
dd29b06260 | ||
|
|
490cb25a0f | ||
|
|
0df0dd741e | ||
|
|
2172946879 | ||
|
|
40e50f0e75 | ||
|
|
65cf0888b5 | ||
|
|
21833019ca | ||
|
|
b3171ba3e9 | ||
|
|
6f01f19d02 | ||
|
|
ce92b01eac | ||
|
|
e24a177434 | ||
|
|
56a52b6d48 | ||
|
|
92bfa8c723 | ||
|
|
2a52aaa4a6 | ||
|
|
8280a3e9d8 | ||
|
|
523f60bf68 | ||
|
|
19b11d4084 | ||
|
|
805bae1507 | ||
|
|
f6c014c06f | ||
|
|
c5794f4596 | ||
|
|
fc28817380 | ||
|
|
80bbd3a165 | ||
|
|
7a10617a72 | ||
|
|
f0b6dca1a5 | ||
|
|
5db20adc34 | ||
|
|
12dc41a517 | ||
|
|
768fd56891 | ||
|
|
8a508cb1cc | ||
|
|
34f6a8eef4 | ||
|
|
c9d2d31a9b | ||
|
|
68fb23a361 | ||
|
|
476139e528 | ||
|
|
6a850818a0 | ||
|
|
3970dbba0d | ||
|
|
8ee2166f0d | ||
|
|
e13500a185 | ||
|
|
411f574a51 | ||
|
|
7fc91b992e | ||
|
|
b840012be0 | ||
|
|
645c51a9dc | ||
|
|
0ce6f05539 | ||
|
|
8b1188af53 | ||
|
|
12b01f8dee | ||
|
|
60f4faf409 | ||
|
|
528dff3f1b | ||
|
|
d429fb4a3e | ||
|
|
816c916709 | ||
|
|
b7a2b8b537 | ||
|
|
261d8cf434 | ||
|
|
41f49bde76 | ||
|
|
65f685bdb2 | ||
|
|
f52a7f4aac | ||
|
|
e71b9903d9 | ||
|
|
325fd08aef | ||
|
|
3888704464 | ||
|
|
38e8a10a29 | ||
|
|
d8545eae12 | ||
|
|
3d5bfd87d2 | ||
|
|
e66c5626bd | ||
|
|
a227c6561e | ||
|
|
e885c159df | ||
|
|
09fcb74bef | ||
|
|
a089197197 | ||
|
|
34f843875b | ||
|
|
ca278a8c39 | ||
|
|
75603786e0 | ||
|
|
4e4e699b94 | ||
|
|
da31fa9fba | ||
|
|
95e2afaf47 | ||
|
|
958a56dde9 | ||
|
|
74cb15930e | ||
|
|
aa37a98424 | ||
|
|
11cbdda3a5 | ||
|
|
6d1f4adf10 | ||
|
|
ddbc50c15e | ||
|
|
b2cbf570a3 | ||
|
|
22b8e65031 | ||
|
|
63c94d2036 | ||
|
|
010df48c1e | ||
|
|
fdc11db93d | ||
|
|
cb4749f168 | ||
|
|
46a8fcf471 |
@@ -40,14 +40,14 @@ runs:
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
run: pnpm run build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm build
|
||||
run: pnpm run build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
3
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
| jq -c --slurp 'map(select(length > 0))')
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/graphql-js": [
|
||||
"../packages/graphql-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
@@ -61,7 +60,6 @@ export default defineConfig({
|
||||
'@apollo/client/utilities': '@apollo/client/utilities',
|
||||
'graphql-ws': 'graphql-ws',
|
||||
xstate: 'xstate',
|
||||
axios: 'axios',
|
||||
'js-cookie': 'Cookies',
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
|
||||
@@ -1,5 +1,97 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.11.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b755e908: fix(dashboard): use correct date for last seen
|
||||
- 2d9145f9: chore(deps): revert GraphQL client
|
||||
- 1ddf704c: fix(dashboard): don't show false positive message for failed user creation
|
||||
- @nhost/react-apollo@5.0.3
|
||||
- @nhost/nextjs@1.13.8
|
||||
|
||||
## 0.11.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.2
|
||||
- @nhost/nextjs@1.13.7
|
||||
|
||||
## 0.11.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2cc18dcb: fix(dashboard): prevent permission editor dropdown from being always open
|
||||
|
||||
## 0.11.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3343a363: chore(dashboard): bump `@testing-library/react` to v14 and `@testing-library/dom` to v9
|
||||
- @nhost/react-apollo@5.0.1
|
||||
- @nhost/nextjs@1.13.6
|
||||
|
||||
## 0.11.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 87eda76e: chore(dashboard): bump `@types/react` to v18.0.28 and `@types/react-dom` to v18.0.11
|
||||
- 6f0ac570: feat(dashboard): show dashboard version in account menu
|
||||
|
||||
## 0.11.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bf1e4071: chore(dashboard): bump `react-is` version to `18.2.0`
|
||||
- Updated dependencies [bf1e4071]
|
||||
- Updated dependencies [5013213b]
|
||||
- @nhost/nextjs@1.13.5
|
||||
- @nhost/react-apollo@4.13.5
|
||||
|
||||
## 0.11.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a37a430b: fix(dashboard): don't break UI when deployments are unavailable
|
||||
- @nhost/react-apollo@4.13.4
|
||||
- @nhost/nextjs@1.13.4
|
||||
|
||||
## 0.11.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7b970e68: fix(dashboard): fix header link color
|
||||
|
||||
## 0.11.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f33242f2: feat(dashboard): add new sign up, sign in and reset password pages
|
||||
|
||||
## 0.11.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e9c8909c: fix(dashboard): use correct theme color in dark mode
|
||||
|
||||
## 0.11.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 902f486b: fix(dashboard): re-enable Hasura on logs page
|
||||
|
||||
## 0.11.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1f9720fa: fix(dashboard): apply select permissions properly
|
||||
|
||||
## 0.11.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- deb14b51: fix(dashboard): don't break billing form
|
||||
|
||||
## 0.11.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
const { version } = require('./package.json');
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
@@ -10,6 +11,9 @@ module.exports = withBundleAnalyzer({
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
},
|
||||
eslint: {
|
||||
dirs: ['src'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -44,7 +44,6 @@
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"analytics-node": "^6.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^1.2.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
@@ -67,7 +66,7 @@
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
@@ -98,15 +97,15 @@
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.10",
|
||||
@@ -129,7 +128,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jsdom": "^21.0.0",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"msw": "^1.0.1",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<svg width="142" height="48" viewBox="0 0 142 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.985 22.8273H83.1731V17.1251H86.9756V31.7529H83.1731V26.0506H75.985V31.7529H72.3164V17.1251H75.985V22.8273ZM101.48 17.1251H95.6099C93.9842 17.1251 92.9947 17.4319 92.2458 18.1792C91.4999 18.9235 91.2137 19.8684 91.2137 21.5362V27.3478C91.2137 29.0157 91.4999 29.9575 92.2458 30.7048C92.9917 31.4522 93.9811 31.759 95.6099 31.759H101.48C103.105 31.759 104.095 31.4522 104.844 30.7048C105.593 29.9605 105.876 29.0157 105.876 27.3478V21.5362C105.876 19.8684 105.59 18.9266 104.844 18.1792C104.095 17.4319 103.105 17.1251 101.48 17.1251ZM102.204 27.4329C102.204 28.2896 101.918 28.5296 100.886 28.5296H96.2036C95.1715 28.5296 94.8853 28.2896 94.8853 27.4329V21.4451C94.8853 20.5884 95.1715 20.3484 96.2036 20.3484H100.886C101.942 20.3484 102.204 20.5671 102.204 21.4451V27.4329ZM114.972 22.9792H120.224C121.85 22.9792 122.861 23.2861 123.588 24.0334C124.246 24.6927 124.62 25.7651 124.62 26.9286V27.8066C124.62 28.9701 124.246 30.0213 123.588 30.7018C122.861 31.4491 121.871 31.756 120.224 31.756H110.202V28.5327H119.609C120.641 28.5327 120.927 28.2927 120.927 27.4359V26.9985C120.927 26.1418 120.641 25.9018 119.609 25.9018H114.357C112.729 25.9018 111.718 25.5919 110.993 24.8476C110.336 24.1884 109.961 23.1159 109.961 21.9524V21.0744C109.961 19.9109 110.333 18.8597 110.993 18.1792C111.721 17.4319 112.707 17.1251 114.357 17.1251H123.856V20.3484H114.972C113.94 20.3484 113.654 20.5884 113.654 21.4451V21.8825C113.654 22.7392 113.94 22.9792 114.972 22.9792ZM126.946 20.3484V17.1251H141.891V20.3484H136.265V31.7529H132.594V20.3484H126.946Z" fill="#21324B"/>
|
||||
<path d="M63.5321 17.0096H57.6624C56.0366 17.0096 55.0471 17.3164 54.2982 18.0638C53.5523 18.8081 53.2661 19.7529 53.2661 21.4177V23.6385V24.322V31.7499H56.9378V24.3251V23.6415V21.3327C56.9378 20.4759 57.224 20.2359 58.256 20.2359H62.9385C63.9949 20.2359 64.2567 20.4547 64.2567 21.3327V23.6415V24.3251V31.7529H67.9284V24.3251V23.6415V21.4208C67.9284 19.7529 67.6422 18.8111 66.8963 18.0668C66.1504 17.3164 65.1609 17.0096 63.5321 17.0096Z" fill="#0052CD"/>
|
||||
<g clip-path="url(#clip1)">
|
||||
<path d="M40.8114 10.2866L24.0819 0.647089C22.581 -0.215696 20.7178 -0.215696 19.2138 0.647089C17.7129 1.51291 16.7813 3.12304 16.7813 4.85165V6.10937L15.6913 5.48051C14.1904 4.61772 12.3272 4.61772 10.8232 5.48051C9.32224 6.34633 8.39063 7.95646 8.39063 9.6881V10.9458L7.3007 10.317C5.79976 9.45418 3.93653 9.45418 2.43255 10.317C0.931612 11.1828 0 12.7929 0 14.5246V44.7281C0 45.597 0.505383 46.4051 1.29086 46.7818C2.0733 47.1615 3.02318 47.0582 3.70515 46.5205L12.0014 39.9919L24.7944 47.362C25.1475 47.5656 25.5433 47.6658 25.9391 47.6658C26.3349 47.6658 26.7307 47.5625 27.0838 47.362C27.7901 46.9549 28.2285 46.1985 28.2285 45.3843V27.2081C28.2285 24.2248 26.6211 21.4481 24.0332 19.9565L19.8379 17.5382V4.85468C19.8379 4.21063 20.185 3.60911 20.7452 3.28709C21.3054 2.96506 21.9995 2.96506 22.5597 3.28709L39.2892 12.9235C40.9363 13.8714 41.9592 15.6425 41.9592 17.5382V40.1711C41.9592 40.8152 41.6121 41.4167 41.052 41.7387L36.6192 44.2937V22.3716C36.6192 19.3884 35.0117 16.6116 32.4239 15.12L22.1243 9.18684V12.7018L30.8986 17.757C32.5456 18.7048 33.5686 20.4729 33.5686 22.3716V45.6091C33.5686 46.4203 34.007 47.1797 34.7133 47.5868C35.0665 47.7904 35.4623 47.8906 35.8581 47.8906C36.2538 47.8906 36.6496 47.7873 37.0028 47.5868L42.5803 44.3727C44.0812 43.5068 45.0128 41.8967 45.0128 40.1651V17.5322C45.0068 14.558 43.3993 11.7782 40.8114 10.2866ZM22.5019 22.5934C24.1489 23.5413 25.1719 25.3094 25.1719 27.2081V44.0689L14.577 37.9656L17.9777 35.2921C19.156 34.3656 19.8318 32.9772 19.8318 31.4795V21.0592L22.5019 22.5934ZM16.7813 19.2972V31.4734C16.7813 32.0324 16.5286 32.5519 16.0902 32.8952L3.05058 43.1544V14.5215C3.05058 13.8775 3.39766 13.2759 3.95784 12.9539C4.51803 12.6319 5.21217 12.6319 5.77236 12.9539L8.39063 14.4608V36.0456L11.4412 33.6456V9.6881C11.4412 9.04405 11.7883 8.44253 12.3485 8.12051C12.9087 7.79848 13.6028 7.79848 14.163 8.12051L16.7813 9.62734V15.7792L13.7307 14.0203V17.5382L16.7813 19.2972Z" fill="#0052CD"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="141.873" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1">
|
||||
<rect width="45.0128" height="47.8906" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#fff" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#3888ff" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#3888ff" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#21324B" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#0052CD" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#0052CD" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
1
dashboard/public/assets/line-grid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="1003" height="644" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity=".24"><mask id="b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="128" y="0" width="921" height="644"><ellipse cx="588.5" cy="322" rx="460.5" ry="322" fill="url(#a)" fill-opacity=".32"/></mask><g mask="url(#b)" stroke="#fff" stroke-opacity=".88"><path d="M141.5 17v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609M-51 33.5h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280"/></g></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 322 -460.5 0 588.5 322)"><stop/><stop offset="1" stop-opacity="0"/></radialGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
dashboard/public/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="119" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M63.32 19.023h5.99V14.27h3.17v12.19h-3.17V21.71h-5.99v4.752h-3.058v-12.19h3.057v4.752h.002Zm21.245-4.752h-4.89c-1.356 0-2.18.255-2.804.879-.622.62-.86 1.408-.86 2.798v4.842c0 1.39.237 2.175.86 2.797.62.623 1.446.88 2.803.88h4.891c1.356 0 2.18-.257 2.804-.88.625-.62.86-1.407.86-2.797v-4.842c0-1.39-.237-2.175-.86-2.8-.625-.622-1.448-.877-2.803-.877Zm.604 8.59c0 .713-.237.913-1.099.913h-3.9c-.86 0-1.1-.2-1.1-.913v-4.99c0-.713.24-.913 1.1-.913h3.9c.882 0 1.1.18 1.1.912v4.991Zm10.64-3.712h4.376c1.355 0 2.198.256 2.804.878.549.55.86 1.444.86 2.413v.732c0 .97-.312 1.845-.86 2.413-.606.622-1.431.879-2.804.879h-8.35v-2.688h7.838c.86 0 1.1-.2 1.1-.912v-.365c0-.714-.24-.914-1.1-.914h-4.375c-1.359 0-2.2-.259-2.804-.879-.549-.55-.861-1.442-.861-2.412v-.732c0-.97.31-1.846.861-2.412.606-.624 1.428-.879 2.803-.879h7.916v2.686h-7.404c-.86 0-1.099.2-1.099.913v.365c0 .715.24.915 1.1.915v-.001Zm9.979-2.192V14.27h12.454v2.688h-4.688v9.503h-3.06v-9.503h-4.707.001Z"/><path d="M52.946 14.175h-4.892c-1.355 0-2.18.255-2.803.879-.622.62-.86 1.407-.86 2.795v8.61h3.058v-8.684c0-.712.24-.912 1.1-.912h3.902c.88 0 1.098.182 1.098.912v8.685h3.06v-8.61c0-1.39-.238-2.175-.86-2.795-.622-.625-1.446-.881-2.803-.881v.001ZM34.01 8.573 20.069.538a4.085 4.085 0 0 0-4.058 0 4.057 4.057 0 0 0-2.026 3.503V5.09l-.909-.525a4.085 4.085 0 0 0-4.057 0 4.058 4.058 0 0 0-2.027 3.51v1.048l-.908-.524a4.085 4.085 0 0 0-4.056 0A4.058 4.058 0 0 0 0 12.104v25.17a1.907 1.907 0 0 0 3.087 1.494L10 33.325l10.663 6.143a1.936 1.936 0 0 0 1.907 0c.587-.34.954-.97.954-1.65V22.676a6.995 6.995 0 0 0-3.497-6.044l-3.496-2.015V4.046A1.513 1.513 0 0 1 18.8 2.738l13.941 8.031a4.452 4.452 0 0 1 2.225 3.845v18.861c0 .538-.29 1.038-.756 1.307l-3.694 2.128V18.643A6.994 6.994 0 0 0 27.02 12.6l-8.582-4.944v2.93l7.31 4.212a4.448 4.448 0 0 1 2.226 3.845v19.365c0 .675.365 1.308.953 1.648a1.936 1.936 0 0 0 1.908 0l4.649-2.678a4.058 4.058 0 0 0 2.027-3.507v-18.86a7.01 7.01 0 0 0-3.501-6.038ZM18.75 18.828a4.448 4.448 0 0 1 2.225 3.846v14.05l-8.828-5.086 2.834-2.228a4.018 4.018 0 0 0 1.545-3.177V17.55l2.225 1.279-.001-.001Zm-4.766-2.747v10.147c0 .466-.211.9-.576 1.185L2.54 35.963V12.1a1.512 1.512 0 0 1 2.268-1.306l2.183 1.256v17.988l2.542-2V8.073a1.511 1.511 0 0 1 2.268-1.307l2.182 1.255v5.128l-2.54-1.465v2.931l2.542 1.466h-.002Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h118.75v40H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 140 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="white" fill-opacity="0.15"/>
|
||||
<rect width="200" height="200" fill="#263245" fill-opacity="0.08"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="white" fill-opacity="0.15"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="#263245" fill-opacity="0.25"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -111,9 +111,9 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { deployments } = deploymentPageData || {};
|
||||
const { deployments } = deploymentPageData || { deployments: [] };
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData || {};
|
||||
scheduledOrPendingDeploymentsData || { deployments: [] };
|
||||
|
||||
const latestDeployment = latestDeploymentData?.deployments[0];
|
||||
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
|
||||
@@ -135,7 +135,7 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
deployment={deployment}
|
||||
isLive={liveDeploymentId === deployment.id}
|
||||
showRedeploy={latestDeployment.id === deployment.id}
|
||||
disableRedeploy={scheduledOrPendingDeployments.length > 0}
|
||||
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
|
||||
/>
|
||||
|
||||
{index !== deployments.length - 1 && <Divider component="li" />}
|
||||
@@ -143,7 +143,7 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
))}
|
||||
</List>
|
||||
<div className="mt-8 flex w-full justify-center">
|
||||
<div className="grid grid-flow-col gap-2 items-center">
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
<NextPrevPageLink
|
||||
direction="prev"
|
||||
prevAllowed={page !== 1}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function ApplicationErrored() {
|
||||
const { currentDate } = useCurrentDate();
|
||||
const user = useUserData();
|
||||
const isOwner = currentWorkspace.members.some(
|
||||
({ userId, type }) => userId === user.id && type === 'owner',
|
||||
({ userId, type }) => userId === user?.id && type === 'owner',
|
||||
);
|
||||
|
||||
const { appCreatedAt } = useAppCreatedAt();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import Container from '@/components/layout/Container';
|
||||
import { features } from '@/components/overview/features';
|
||||
import { frameworks } from '@/components/overview/frameworks';
|
||||
@@ -55,7 +56,9 @@ export default function ApplicationLive() {
|
||||
|
||||
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
||||
<div className="order-2 grid grid-flow-row gap-12 lg:order-1 lg:col-span-2">
|
||||
<OverviewDeployments />
|
||||
<RetryableErrorBoundary>
|
||||
<OverviewDeployments />
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<OverviewDocumentation
|
||||
title="Pick your favorite framework and start learning"
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ApplicationUnknown() {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const user = useUserData();
|
||||
const isOwner = currentWorkspace.members.some(
|
||||
({ userId, type }) => userId === user.id && type === 'owner',
|
||||
({ userId, type }) => userId === user?.id && type === 'owner',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,6 +31,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
<NavLink
|
||||
href="/local/local"
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
local
|
||||
</NavLink>
|
||||
@@ -53,6 +54,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}`}
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{currentWorkspace.name}
|
||||
</NavLink>
|
||||
@@ -66,6 +68,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}`}
|
||||
className="truncate text-[13px] hover:underline sm:text-sm"
|
||||
sx={{ color: 'text.primary' }}
|
||||
>
|
||||
{currentApplication.name}
|
||||
</NavLink>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import Breadcrumbs from '@/components/common/Breadcrumbs';
|
||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||
import LocalAccountMenu from '@/components/common/LocalAccountMenu';
|
||||
import Logo from '@/components/common/Logo';
|
||||
import MobileNav from '@/components/common/MobileNav';
|
||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import { AccountMenu } from '@/components/dashboard/AccountMenu';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NavLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -32,10 +31,8 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
{...props}
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-3">
|
||||
<NavLink href="/" passHref>
|
||||
<Link href="/" className="w-12">
|
||||
<Logo className="mx-auto cursor-pointer" />
|
||||
</Link>
|
||||
<NavLink href="/" className="w-12">
|
||||
<Logo className="mx-auto cursor-pointer" />
|
||||
</NavLink>
|
||||
|
||||
{(router.query.workspaceSlug || router.query.appSlug) && (
|
||||
@@ -63,25 +60,20 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
)}
|
||||
|
||||
<NavLink
|
||||
underline="none"
|
||||
href="https://docs.nhost.io"
|
||||
passHref
|
||||
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { backgroundColor: 'grey.200' },
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Link
|
||||
underline="none"
|
||||
href="https://docs.nhost.io"
|
||||
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': { backgroundColor: 'grey.200' },
|
||||
}}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
Docs
|
||||
</NavLink>
|
||||
|
||||
{isPlatform ? <AccountMenu /> : <ThemeSwitcher className="w-52" />}
|
||||
{isPlatform ? <AccountMenu /> : <LocalAccountMenu />}
|
||||
</div>
|
||||
|
||||
<MobileNav className="sm:hidden" />
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { NavLinkProps } from '@/components/common/NavLink';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import type { SvgIconProps } from '@/ui/v2/icons/SvgIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NavLink from 'next/link';
|
||||
import type { ForwardedRef, ReactElement } from 'react';
|
||||
import type { ForwardedRef, PropsWithoutRef, ReactElement } from 'react';
|
||||
import { cloneElement, forwardRef, isValidElement } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface IconLinkProps extends Omit<NavLinkProps, 'ref'> {
|
||||
export interface IconLinkProps extends PropsWithoutRef<NavLinkProps> {
|
||||
/**
|
||||
* The icon to display.
|
||||
*/
|
||||
@@ -58,49 +57,49 @@ function IconLink(
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink ref={ref} passHref href={href} {...props}>
|
||||
<Link
|
||||
href={href}
|
||||
underline="none"
|
||||
className={twMerge(
|
||||
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
|
||||
className,
|
||||
)}
|
||||
sx={{
|
||||
fontSize: (theme) => theme.typography.pxToRem(10),
|
||||
lineHeight: (theme) => theme.typography.pxToRem(15),
|
||||
backgroundColor: active ? 'primary.light' : 'transparent',
|
||||
color: active ? 'primary.main' : 'text.primary',
|
||||
[`&:hover`]: {
|
||||
backgroundColor: active ? 'primary.light' : 'action.hover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
...icon.props,
|
||||
className: twMerge('w-4 h-4', icon.props.className),
|
||||
sx: [
|
||||
...(Array.isArray(icon.props?.sx)
|
||||
? icon.props.sx
|
||||
: [icon.props?.sx]),
|
||||
{
|
||||
color: (theme) => {
|
||||
if (active) {
|
||||
return 'primary.main';
|
||||
}
|
||||
<NavLink
|
||||
ref={ref}
|
||||
href={href}
|
||||
underline="none"
|
||||
className={twMerge(
|
||||
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
|
||||
className,
|
||||
)}
|
||||
sx={{
|
||||
fontSize: (theme) => theme.typography.pxToRem(10),
|
||||
lineHeight: (theme) => theme.typography.pxToRem(15),
|
||||
backgroundColor: active ? 'primary.light' : 'transparent',
|
||||
color: active ? 'primary.main' : 'text.primary',
|
||||
[`&:hover`]: {
|
||||
backgroundColor: active ? 'primary.light' : 'action.hover',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
...icon.props,
|
||||
className: twMerge('w-4 h-4', icon.props.className),
|
||||
sx: [
|
||||
...(Array.isArray(icon.props?.sx)
|
||||
? icon.props.sx
|
||||
: [icon.props?.sx]),
|
||||
{
|
||||
color: (theme) => {
|
||||
if (active) {
|
||||
return 'primary.main';
|
||||
}
|
||||
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.primary';
|
||||
},
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'text.secondary'
|
||||
: 'text.primary';
|
||||
},
|
||||
],
|
||||
})
|
||||
: null}
|
||||
},
|
||||
],
|
||||
})
|
||||
: null}
|
||||
|
||||
{children}
|
||||
</Link>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
import type { ActivityIndicatorProps } from '@/ui/v2/ActivityIndicator';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { BoxProps } from '@/ui/v2/Box';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function LoadingScreen() {
|
||||
export interface LoadingScreenProps extends BoxProps {
|
||||
/**
|
||||
* Props passed to individual component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to the `<Box />` component.
|
||||
*/
|
||||
root?: BoxProps;
|
||||
/**
|
||||
* Props passed to the `<ActivityIndicator />` component.
|
||||
*/
|
||||
activityIndicator?: ActivityIndicatorProps;
|
||||
};
|
||||
}
|
||||
|
||||
export function LoadingScreen({
|
||||
className,
|
||||
slotProps = { root: {}, activityIndicator: {} },
|
||||
...props
|
||||
}: LoadingScreenProps) {
|
||||
return (
|
||||
<Box className="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center z-50 h-full w-full">
|
||||
<ActivityIndicator circularProgressProps={{ className: 'w-5 h-5' }} />
|
||||
<Box
|
||||
className={twMerge(
|
||||
'absolute top-0 left-0 bottom-0 right-0 z-50 flex h-full w-full items-center justify-center',
|
||||
className,
|
||||
slotProps?.root?.className,
|
||||
)}
|
||||
{...slotProps?.root}
|
||||
{...props}
|
||||
>
|
||||
<ActivityIndicator
|
||||
{...slotProps?.activityIndicator}
|
||||
circularProgressProps={{
|
||||
...slotProps?.activityIndicator?.circularProgressProps,
|
||||
className: twMerge(
|
||||
'w-5 h-5',
|
||||
slotProps?.activityIndicator?.circularProgressProps?.className,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import UserIcon from '@/ui/v2/icons/UserIcon';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getConfig from 'next/config';
|
||||
|
||||
export default function LocalAccountMenu() {
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
return (
|
||||
<Dropdown.Root className="justify-self-center">
|
||||
<Dropdown.Trigger hideChevron asChild>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="h-7 w-7 rounded-full"
|
||||
sx={{
|
||||
backgroundColor: (theme) => `${theme.palette.grey[300]} !important`,
|
||||
}}
|
||||
>
|
||||
<UserIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
PaperProps={{
|
||||
className: 'mt-1 p-6 grid grid-flow-row gap-4 w-full max-w-xs',
|
||||
}}
|
||||
>
|
||||
<ThemeSwitcher label="Theme" />
|
||||
|
||||
<Text className="text-center text-xs" color="disabled">
|
||||
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||
</Text>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LocalAccountMenu';
|
||||
@@ -20,6 +20,7 @@ import type { ListItemButtonProps } from '@/ui/v2/ListItem';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useSignOut } from '@nhost/nextjs';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cloneElement, Fragment, isValidElement, useState } from 'react';
|
||||
@@ -89,6 +90,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
const { signOut } = useSignOut();
|
||||
const { setUserContext } = useUserDataContext();
|
||||
const router = useRouter();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -256,6 +258,10 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
||||
</ListItem.Button>
|
||||
</ListItem.Root>
|
||||
</List>
|
||||
|
||||
<Text className="text-center text-xs" color="secondary">
|
||||
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||
</Text>
|
||||
</section>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import Link from 'next/link';
|
||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import type { LinkProps } from '@/ui/v2/Link';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import NextLink from 'next/link';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface NavLinkProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLAnchorElement>, HTMLAnchorElement> {}
|
||||
export interface NavLinkProps extends PropsWithoutRef<LinkProps> {
|
||||
/**
|
||||
* Determines whether or not the link should be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function NavLink(
|
||||
{ className, children, href, ...props }: NavLinkProps,
|
||||
ref: ForwardedRef<HTMLAnchorElement>,
|
||||
) {
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a className={twMerge('font-display', className)} ref={ref} {...props}>
|
||||
<NextLink href={href} passHref>
|
||||
<Link className={twMerge('font-display', className)} ref={ref} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ export default function ThemeSwitcher({
|
||||
|
||||
onChange?.(event, value);
|
||||
}}
|
||||
slotProps={{
|
||||
listbox: { className: 'min-w-0 w-full' },
|
||||
popper: {
|
||||
disablePortal: false,
|
||||
className: 'z-[10000] w-[270px] w-full',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Option value="light">Light</Option>
|
||||
<Option value="dark">Dark</Option>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Text from '@/ui/v2/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import getConfig from 'next/config';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -24,23 +25,10 @@ function AccountMenuContent({
|
||||
const router = useRouter();
|
||||
const client = useApolloClient();
|
||||
const { handleClose } = useDropdown();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
return (
|
||||
<Box className="relative grid w-account grid-flow-row gap-5 p-6">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="absolute top-6 right-4 grid grid-flow-col items-center gap-px self-start font-medium"
|
||||
onClick={async () => {
|
||||
router.push('/signin');
|
||||
await nhost.auth.signOut();
|
||||
await client.resetStore();
|
||||
}}
|
||||
endIcon={<PowerIcon className="w-4 h-4 mr-1" />}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
|
||||
<Box className="relative grid w-full grid-flow-row gap-5 p-6">
|
||||
<div className="grid grid-flow-row justify-center">
|
||||
<Avatar
|
||||
className="mx-auto mb-2 h-16 w-16 rounded-full"
|
||||
@@ -72,9 +60,26 @@ function AccountMenuContent({
|
||||
<Button color="error" disabled>
|
||||
Remove Account
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={async () => {
|
||||
await nhost.auth.signOut();
|
||||
router.push('/signin');
|
||||
await client.resetStore();
|
||||
}}
|
||||
endIcon={<PowerIcon className="mr-1 h-4 w-4" />}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ThemeSwitcher label="Theme" fullWidth />
|
||||
<ThemeSwitcher label="Theme" />
|
||||
|
||||
<Text className="text-center text-xs" color="disabled">
|
||||
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -107,7 +112,7 @@ export function AccountMenu() {
|
||||
/>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content PaperProps={{ className: 'mt-1' }}>
|
||||
<Dropdown.Content PaperProps={{ className: 'mt-1 max-w-xs w-full' }}>
|
||||
<AccountMenuContent
|
||||
onChangePasswordClick={() => setChangePasswordModal(true)}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function CountrySelector({ value, onChange }: CountrySelectorProps) {
|
||||
placeholder="Select Country"
|
||||
>
|
||||
{countries?.map((country) => (
|
||||
<Option key={country.name} value={country.name}>
|
||||
<Option key={country.name} value={country.code}>
|
||||
{country.name}
|
||||
</Option>
|
||||
))}
|
||||
|
||||
@@ -181,6 +181,7 @@ export default function EditPermissionsForm({
|
||||
|
||||
return (
|
||||
<RolePermissionEditorForm
|
||||
resourceVersion={metadata?.resourceVersion}
|
||||
disabled={disabled}
|
||||
schema={schema}
|
||||
table={table}
|
||||
@@ -203,7 +204,7 @@ export default function EditPermissionsForm({
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
>
|
||||
<div className="flex-auto">
|
||||
<Box className="grid grid-flow-row gap-6 content-start overflow-y-auto p-6 border-b-1">
|
||||
<Box className="grid grid-flow-row content-start gap-6 overflow-y-auto border-b-1 p-6">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Text component="h2" className="!font-bold">
|
||||
Roles & Actions overview
|
||||
@@ -215,24 +216,24 @@ export default function EditPermissionsForm({
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-col gap-4 items-center justify-start">
|
||||
<div className="grid grid-flow-col items-center justify-start gap-4">
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="grid items-center grid-flow-col gap-1"
|
||||
className="grid grid-flow-col items-center gap-1"
|
||||
>
|
||||
full access <FullPermissionIcon />
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="grid items-center grid-flow-col gap-1"
|
||||
className="grid grid-flow-col items-center gap-1"
|
||||
>
|
||||
partial access <PartialPermissionIcon />
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="grid items-center grid-flow-col gap-1"
|
||||
className="grid grid-flow-col items-center gap-1"
|
||||
>
|
||||
no access <NoPermissionIcon />
|
||||
</Text>
|
||||
@@ -262,7 +263,7 @@ export default function EditPermissionsForm({
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody className="rounded-sm+ block border-1">
|
||||
<TableBody className="block rounded-sm+ border-1">
|
||||
<RolePermissionsRow
|
||||
name="admin"
|
||||
disabled
|
||||
|
||||
@@ -66,6 +66,10 @@ export interface RolePermissionEditorFormValues {
|
||||
* Whether the mutation should be restricted to trusted backends.
|
||||
*/
|
||||
backendOnly?: boolean;
|
||||
/**
|
||||
* Computed fields to be allowed for the role.
|
||||
*/
|
||||
computedFields?: string[];
|
||||
}
|
||||
|
||||
export interface RolePermissionEditorFormProps {
|
||||
@@ -85,6 +89,10 @@ export interface RolePermissionEditorFormProps {
|
||||
* The role that is being edited.
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* The resource version of the metadata.
|
||||
*/
|
||||
resourceVersion: number;
|
||||
/**
|
||||
* The action that is being edited.
|
||||
*/
|
||||
@@ -155,6 +163,7 @@ export default function RolePermissionEditorForm({
|
||||
schema,
|
||||
table,
|
||||
role,
|
||||
resourceVersion,
|
||||
action,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
@@ -189,6 +198,7 @@ export default function RolePermissionEditorForm({
|
||||
permission?.subscription_root_fields?.length > 0,
|
||||
queryRootFields: permission?.query_root_fields || [],
|
||||
subscriptionRootFields: permission?.subscription_root_fields || [],
|
||||
computedFields: permission?.computed_fields || [],
|
||||
columnPresets: getColumnPresets(permission?.set || {}),
|
||||
backendOnly: permission?.backend_only || false,
|
||||
},
|
||||
@@ -213,13 +223,18 @@ export default function RolePermissionEditorForm({
|
||||
action,
|
||||
mode: permission ? 'update' : 'insert',
|
||||
originalPermission: permission,
|
||||
resourceVersion,
|
||||
permission: {
|
||||
set: convertToColumnPresetObject(values.columnPresets),
|
||||
columns: values.columns,
|
||||
limit: values.limit,
|
||||
allow_aggregations: values.allowAggregations,
|
||||
query_root_fields: values.queryRootFields,
|
||||
subscription_root_fields: values.subscriptionRootFields,
|
||||
query_root_fields:
|
||||
values.queryRootFields.length > 0 ? values.queryRootFields : null,
|
||||
subscription_root_fields:
|
||||
values.subscriptionRootFields.length > 0
|
||||
? values.subscriptionRootFields
|
||||
: null,
|
||||
filter:
|
||||
action !== 'insert'
|
||||
? convertToHasuraPermissions(values.filter as RuleGroup)
|
||||
@@ -229,6 +244,10 @@ export default function RolePermissionEditorForm({
|
||||
? convertToHasuraPermissions(values.filter as RuleGroup)
|
||||
: permission?.check,
|
||||
backend_only: values.backendOnly,
|
||||
computed_fields:
|
||||
permission?.computed_fields.length > 0
|
||||
? permission?.computed_fields
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -266,6 +285,7 @@ export default function RolePermissionEditorForm({
|
||||
const deletePermissionPromise = managePermission({
|
||||
role,
|
||||
action,
|
||||
resourceVersion,
|
||||
originalPermission: permission,
|
||||
mode: 'delete',
|
||||
});
|
||||
@@ -331,10 +351,10 @@ export default function RolePermissionEditorForm({
|
||||
className="flex flex-auto flex-col content-between overflow-hidden border-t-1"
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-6 content-start flex-auto py-4 overflow-auto">
|
||||
<div className="grid flex-auto grid-flow-row content-start gap-6 overflow-auto py-4">
|
||||
<PermissionSettingsSection
|
||||
title="Selected role & action"
|
||||
className="justify-between grid-flow-col"
|
||||
className="grid-flow-col justify-between"
|
||||
>
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Text>
|
||||
@@ -387,7 +407,7 @@ export default function RolePermissionEditorForm({
|
||||
{action !== 'select' && <BackendOnlySection disabled={disabled} />}
|
||||
</div>
|
||||
|
||||
<Box className="grid flex-shrink-0 sm:grid-flow-col sm:justify-between gap-2 border-t-1 p-2">
|
||||
<Box className="grid flex-shrink-0 gap-2 border-t-1 p-2 sm:grid-flow-col sm:justify-between">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
@@ -398,7 +418,7 @@ export default function RolePermissionEditorForm({
|
||||
</Button>
|
||||
|
||||
{!disabled && (
|
||||
<Box className="grid grid-flow-row sm:grid-flow-col gap-2">
|
||||
<Box className="grid grid-flow-row gap-2 sm:grid-flow-col">
|
||||
{Boolean(permission) && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -11,7 +11,6 @@ import { inputClasses } from '@/ui/v2/Input';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
||||
import clsx from 'clsx';
|
||||
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
||||
import useRuleGroupEditor from './useRuleGroupEditor';
|
||||
|
||||
@@ -214,7 +213,6 @@ export default function RuleValueInput({
|
||||
freeSolo={!isHasuraInput}
|
||||
autoSelect={!isHasuraInput}
|
||||
autoHighlight={isHasuraInput}
|
||||
filterSelectedOptions
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return option.value.toLowerCase() === (value as string).toLowerCase();
|
||||
@@ -230,7 +228,7 @@ export default function RuleValueInput({
|
||||
sx: sharedInputSx,
|
||||
},
|
||||
formControl: { className: '!bg-transparent' },
|
||||
paper: { className: clsx(!isHasuraInput && 'hidden') },
|
||||
paper: { className: 'empty:border-transparent' },
|
||||
}}
|
||||
fullWidth
|
||||
loading={loading}
|
||||
|
||||
@@ -58,9 +58,10 @@ export default function DeploymentListItem({
|
||||
return (
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
|
||||
className="grid grid-flow-col items-center justify-between gap-2 rounded-none px-2 py-2"
|
||||
component={NavLink}
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
||||
aria-label={commitMessage || 'No commit message'}
|
||||
>
|
||||
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
|
||||
<ListItem.Avatar>
|
||||
@@ -83,10 +84,14 @@ export default function DeploymentListItem({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-col gap-2 items-center">
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
{showRedeploy && (
|
||||
<Tooltip
|
||||
title="Deployments cannot be re-triggered when a deployment is in progress."
|
||||
title={
|
||||
!disableRedeploy && !loading
|
||||
? 'Deployments cannot be re-triggered when a deployment is in progress.'
|
||||
: ''
|
||||
}
|
||||
hasDisabledChildren={disableRedeploy || loading}
|
||||
disableHoverListener={!disableRedeploy}
|
||||
>
|
||||
@@ -123,9 +128,10 @@ export default function DeploymentListItem({
|
||||
);
|
||||
}}
|
||||
startIcon={
|
||||
<ArrowCounterclockwiseIcon className={twMerge('w-4 h-4')} />
|
||||
<ArrowCounterclockwiseIcon className={twMerge('h-4 w-4')} />
|
||||
}
|
||||
className="rounded-full py-1 px-2 text-xs"
|
||||
aria-label="Redeploy"
|
||||
>
|
||||
Redeploy
|
||||
</Button>
|
||||
@@ -133,7 +139,7 @@ export default function DeploymentListItem({
|
||||
)}
|
||||
|
||||
{isLive && (
|
||||
<div className="w-12 flex justify-end">
|
||||
<div className="flex w-12 justify-end">
|
||||
<Chip size="small" color="success" label="Live" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -277,7 +277,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
}
|
||||
|
||||
if (fileError) {
|
||||
throw fileError;
|
||||
throw new Error(fileError.message);
|
||||
}
|
||||
|
||||
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
||||
|
||||
@@ -67,7 +67,7 @@ export function InviteAnnounce() {
|
||||
triggerToast('An error occurred when trying to accept the invitation.');
|
||||
|
||||
return setSubmitState({
|
||||
error: res.error,
|
||||
error: new Error(res.error.message),
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,12 +45,6 @@ export default function AuthenticatedLayout({
|
||||
return;
|
||||
}
|
||||
|
||||
if (router.pathname === '/') {
|
||||
router.push('/signup');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/signin');
|
||||
}, [isLoading, isAuthenticated, router, isPlatform]);
|
||||
|
||||
@@ -84,7 +78,7 @@ export default function AuthenticatedLayout({
|
||||
|
||||
<Container
|
||||
rootClassName="h-full"
|
||||
className="grid justify-center max-w-md grid-flow-row gap-2 my-12 text-center"
|
||||
className="my-12 grid max-w-md grid-flow-row justify-center gap-2 text-center"
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
@@ -121,7 +115,7 @@ export default function AuthenticatedLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout className="flex flex-col h-full" {...props}>
|
||||
<BaseLayout className="flex h-full flex-col" {...props}>
|
||||
<Header className="flex max-h-[59px] flex-auto" />
|
||||
|
||||
<InviteAnnounce />
|
||||
|
||||
@@ -2,9 +2,14 @@ import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
|
||||
import BaseLayout from '@/components/layout/BaseLayout';
|
||||
import Container from '@/components/layout/Container';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCleanWorkspaceContext } from '@/hooks/use-cleanWorkspaceContext';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import ThemeProvider from '@/ui/v2/ThemeProvider';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import { useAuthenticationStatus } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -28,14 +33,76 @@ export default function UnauthenticatedLayout({
|
||||
if (!isPlatform || isLoading || isAuthenticated) {
|
||||
return (
|
||||
<BaseLayout {...props}>
|
||||
<LoadingScreen />
|
||||
<LoadingScreen
|
||||
sx={{ backgroundColor: (theme) => theme.palette.background.default }}
|
||||
slotProps={{
|
||||
activityIndicator: {
|
||||
className: 'text-white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout {...props}>
|
||||
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
|
||||
</BaseLayout>
|
||||
<ThemeProvider color="dark">
|
||||
<BaseLayout {...props}>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
'html, body': {
|
||||
backgroundColor: `#000 !important`,
|
||||
},
|
||||
'#__next': {
|
||||
overflow: 'auto',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<RetryableErrorBoundary>
|
||||
<Box
|
||||
className="flex min-h-screen items-center"
|
||||
sx={{ backgroundColor: (theme) => theme.palette.common.black }}
|
||||
>
|
||||
<Container
|
||||
rootClassName="bg-transparent h-full"
|
||||
className="grid h-full w-full items-center justify-items-center gap-12 bg-transparent pt-8 pb-12 lg:grid-cols-2 lg:gap-4 lg:pt-0 lg:pb-0"
|
||||
>
|
||||
<div className="relative z-10 order-2 grid w-full max-w-[544px] grid-flow-row gap-12 lg:order-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="relative z-0 order-1 flex h-full w-full items-center justify-center md:min-h-[150px] lg:order-2 lg:min-h-[none]">
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 mx-auto flex h-full w-full max-w-xl items-center justify-center opacity-70">
|
||||
<Image
|
||||
priority
|
||||
src="/assets/line-grid.svg"
|
||||
width={1003}
|
||||
height={644}
|
||||
alt="Transparent lines"
|
||||
objectFit="fill"
|
||||
className="h-full w-full scale-[200%]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Box
|
||||
className="backface-hidden absolute left-0 right-0 z-0 mx-auto h-20 w-20 transform-gpu rounded-full opacity-80 blur-[56px]"
|
||||
sx={{
|
||||
backgroundColor: (theme) => theme.palette.primary.main,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Image
|
||||
src="/assets/logo.svg"
|
||||
width={119}
|
||||
height={40}
|
||||
alt="Nhost Logo"
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
</Box>
|
||||
</RetryableErrorBoundary>
|
||||
</BaseLayout>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ export default function LogsHeader({
|
||||
key={value}
|
||||
value={value}
|
||||
className="text-sm+ font-medium"
|
||||
disabled={label === 'Hasura'}
|
||||
>
|
||||
{label}
|
||||
</Option>
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { UserDataProvider } from '@/context/workspace1-context';
|
||||
import type { Application } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import type { Workspace } from '@/types/workspace';
|
||||
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
|
||||
import { graphql, rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import OverviewDeployments from '.';
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn().mockReturnValue({
|
||||
basePath: '',
|
||||
pathname: '/test-workspace/test-application',
|
||||
route: '/[workspaceSlug]/[appSlug]',
|
||||
asPath: '/test-workspace/test-application',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
workspaceSlug: 'test-workspace',
|
||||
appSlug: 'test-application',
|
||||
},
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
back: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
beforePopState: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockApplication: Application = {
|
||||
id: '1',
|
||||
name: 'Test Application',
|
||||
slug: 'test-application',
|
||||
appStates: [],
|
||||
hasuraGraphqlAdminSecret: 'nhost-admin-secret',
|
||||
subdomain: '',
|
||||
isProvisioned: true,
|
||||
region: {
|
||||
awsName: 'us-east-1',
|
||||
city: 'New York',
|
||||
countryCode: 'US',
|
||||
id: '1',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
deployments: [],
|
||||
desiredState: ApplicationStatus.Live,
|
||||
featureFlags: [],
|
||||
providersUpdated: true,
|
||||
githubRepository: { fullName: 'test/git-project' },
|
||||
};
|
||||
|
||||
const mockWorkspace: Workspace = {
|
||||
id: '1',
|
||||
name: 'Test Workspace',
|
||||
slug: 'test-workspace',
|
||||
members: [],
|
||||
applications: [mockApplication],
|
||||
};
|
||||
|
||||
const mockGraphqlLink = graphql.link('http://localhost:1337/v1/graphql');
|
||||
|
||||
const server = setupServer(
|
||||
rest.get('http://localhost:1337/v1/graphql', (req, res, ctx) =>
|
||||
res(ctx.status(200)),
|
||||
),
|
||||
mockGraphqlLink.operation(async (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
deployments: [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should render an empty state when GitHub is not connected', () => {
|
||||
render(
|
||||
<UserDataProvider
|
||||
initialWorkspaces={[
|
||||
{
|
||||
...mockWorkspace,
|
||||
applications: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<OverviewDeployments />
|
||||
</UserDataProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /connect to github/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
|
||||
render(
|
||||
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
|
||||
<OverviewDeployments />
|
||||
</UserDataProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/^deployments$/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /view all/i })).toBeInTheDocument();
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
|
||||
|
||||
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/test\/git-project/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /edit/i })).toHaveAttribute(
|
||||
'href',
|
||||
'/test-workspace/test-application/settings/git',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render a list of deployments', async () => {
|
||||
server.use(
|
||||
mockGraphqlLink.operation(async (req, res, ctx) => {
|
||||
const requestPayload = await req.json();
|
||||
|
||||
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return res(ctx.data({ deployments: [] }));
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.data({
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
|
||||
<OverviewDeployments />
|
||||
</UserDataProvider>,
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
|
||||
|
||||
expect(screen.getByText(/test commit message/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/avatar/i)).toHaveStyle(
|
||||
'background-image: url(http://images.example.com/avatar.png)',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('link', {
|
||||
name: /test commit message/i,
|
||||
}),
|
||||
).toHaveAttribute('href', '/test-workspace/test-application/deployments/1');
|
||||
expect(screen.getByText(/5m 0s/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/live/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /redeploy/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('should disable redeployments if a deployment is already in progress', async () => {
|
||||
server.use(
|
||||
mockGraphqlLink.operation(async (req, res, ctx) => {
|
||||
const requestPayload = await req.json();
|
||||
|
||||
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return res(
|
||||
ctx.data({
|
||||
deployments: [
|
||||
{
|
||||
id: '2',
|
||||
commitSHA: 'abc234',
|
||||
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
|
||||
deploymentEndedAt: null,
|
||||
deploymentStatus: 'PENDING',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.data({
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
|
||||
<OverviewDeployments />
|
||||
</UserDataProvider>,
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
|
||||
expect(screen.getByRole('button', { name: /redeploy/i })).toBeDisabled();
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||
import RocketIcon from '@/ui/v2/icons/RocketIcon';
|
||||
import List from '@/ui/v2/List';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
useGetDeploymentsSubSubscription,
|
||||
useScheduledOrPendingDeploymentsSubSubscription,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import NavLink from 'next/link';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
@@ -22,16 +22,16 @@ function OverviewDeploymentsTopBar() {
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { githubRepository } = currentApplication;
|
||||
const { githubRepository } = currentApplication || {};
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-col gap-2 items-center place-content-between pb-4">
|
||||
<div className="grid grid-flow-col place-content-between items-center gap-2 pb-4">
|
||||
<Text variant="h3" className="font-medium">
|
||||
Deployments
|
||||
</Text>
|
||||
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments`}
|
||||
href={`/${currentWorkspace?.slug}/${currentApplication?.slug}/deployments`}
|
||||
passHref
|
||||
>
|
||||
<Button variant="borderless" disabled={!githubRepository}>
|
||||
@@ -43,20 +43,12 @@ function OverviewDeploymentsTopBar() {
|
||||
);
|
||||
}
|
||||
|
||||
interface OverviewDeploymentsProps {
|
||||
projectId: string;
|
||||
githubRepository: { fullName: string };
|
||||
}
|
||||
|
||||
function OverviewDeployments({
|
||||
projectId,
|
||||
githubRepository,
|
||||
}: OverviewDeploymentsProps) {
|
||||
function OverviewDeploymentList() {
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const { data, loading } = useGetDeploymentsSubSubscription({
|
||||
variables: {
|
||||
id: projectId,
|
||||
id: currentApplication?.id,
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
},
|
||||
@@ -67,23 +59,23 @@ function OverviewDeployments({
|
||||
loading: scheduledOrPendingDeploymentsLoading,
|
||||
} = useScheduledOrPendingDeploymentsSubSubscription({
|
||||
variables: {
|
||||
appId: projectId,
|
||||
appId: currentApplication?.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading || scheduledOrPendingDeploymentsLoading) {
|
||||
return (
|
||||
<Box className="h-[323px] p-2 border-1 rounded-lg">
|
||||
<Box className="h-[323px] rounded-lg border-1 p-2">
|
||||
<ActivityIndicator label="Loading deployments..." />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const { deployments } = data;
|
||||
const { deployments } = data || { deployments: [] };
|
||||
|
||||
if (deployments.length === 0) {
|
||||
if (!deployments?.length) {
|
||||
return (
|
||||
<Box className="grid grid-flow-row gap-5 items-center justify-items-center rounded-lg py-12 px-48 shadow-sm border-1">
|
||||
<Box className="grid grid-flow-row items-center justify-items-center gap-5 overflow-hidden rounded-lg border-1 py-12 px-48 shadow-sm">
|
||||
<RocketIcon
|
||||
strokeWidth={1}
|
||||
className="h-10 w-10"
|
||||
@@ -100,16 +92,16 @@ function OverviewDeployments({
|
||||
</div>
|
||||
|
||||
<Box
|
||||
className="mt-6 flex flex-row place-content-between rounded-lg py-2 px-2 max-w-sm w-full"
|
||||
className="mt-6 flex w-full max-w-sm flex-row place-content-between rounded-lg py-2 px-2"
|
||||
sx={{ backgroundColor: 'grey.200' }}
|
||||
>
|
||||
<Box
|
||||
className="grid grid-flow-col gap-1.5 ml-2"
|
||||
className="ml-2 grid grid-flow-col gap-1.5"
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
<GithubIcon className="h-4 w-4 self-center" />
|
||||
<Text variant="body1" className="self-center font-normal">
|
||||
{githubRepository.fullName}
|
||||
{currentApplication?.githubRepository?.fullName}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -128,20 +120,20 @@ function OverviewDeployments({
|
||||
|
||||
const liveDeploymentId = getLastLiveDeployment(deployments);
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData;
|
||||
scheduledOrPendingDeploymentsData || { deployments: [] };
|
||||
|
||||
return (
|
||||
<List
|
||||
className="rounded-x-lg flex flex-col rounded-lg"
|
||||
className="rounded-x-lg flex flex-col overflow-hidden rounded-lg"
|
||||
sx={{ borderColor: 'grey.300', borderWidth: 1 }}
|
||||
>
|
||||
{deployments.map((deployment, index) => (
|
||||
{deployments?.map((deployment, index) => (
|
||||
<Fragment key={deployment.id}>
|
||||
<DeploymentListItem
|
||||
deployment={deployment}
|
||||
isLive={deployment.id === liveDeploymentId}
|
||||
showRedeploy={index === 0}
|
||||
disableRedeploy={scheduledOrPendingDeployments.length > 0}
|
||||
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
|
||||
/>
|
||||
|
||||
{index !== deployments.length - 1 && <Divider component="li" />}
|
||||
@@ -151,21 +143,18 @@ function OverviewDeployments({
|
||||
);
|
||||
}
|
||||
|
||||
export default function OverviewDeploymentsPage() {
|
||||
export default function OverviewDeployments() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { openGitHubModal } = useGitHubModal();
|
||||
|
||||
const { githubRepository } = currentApplication;
|
||||
const { githubRepository } = currentApplication || {};
|
||||
|
||||
// GitHub repo connected. Show deployments
|
||||
if (githubRepository) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<OverviewDeploymentsTopBar />
|
||||
<OverviewDeployments
|
||||
projectId={currentApplication.id}
|
||||
githubRepository={githubRepository}
|
||||
/>
|
||||
<OverviewDeploymentList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -175,7 +164,7 @@ export default function OverviewDeploymentsPage() {
|
||||
<div className="flex flex-col">
|
||||
<OverviewDeploymentsTopBar />
|
||||
|
||||
<Box className="grid grid-flow-row gap-5 items-center justify-items-center rounded-lg py-12 px-48 shadow-sm border-1">
|
||||
<Box className="grid grid-flow-row items-center justify-items-center gap-5 rounded-lg border-1 py-12 px-48 shadow-sm">
|
||||
<RocketIcon strokeWidth={1} className="h-10 w-10" />
|
||||
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
),
|
||||
},
|
||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.httpUrl },
|
||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
||||
];
|
||||
|
||||
@@ -59,6 +59,7 @@ export function Avatar({
|
||||
<Box
|
||||
style={Object.assign(style, { backgroundImage: `url(${avatarUrl})` })}
|
||||
className={classes}
|
||||
aria-label="Avatar"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -63,7 +63,9 @@ function ActivityIndicator({
|
||||
// We are rendering a span instead of null in order to keep the layout
|
||||
// intact in certain cases (e.g. when elements have a "space-between"
|
||||
// position).
|
||||
return <span />;
|
||||
return (
|
||||
<span role="progressbar" aria-label="Activity indicator placeholder" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ const StyledMenu = styled(MaterialMenu)(({ theme }) => ({
|
||||
borderColor:
|
||||
theme.palette.mode === 'dark'
|
||||
? `${theme.palette.grey[400]} !important`
|
||||
: 'none',
|
||||
: 'transparent',
|
||||
boxShadow:
|
||||
theme.palette.mode === 'light'
|
||||
? '0px 4px 10px rgba(33, 50, 75, 0.25)'
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import type { LinkProps as MaterialLinkProps } from '@mui/material/Link';
|
||||
import MaterialLink from '@mui/material/Link';
|
||||
import MaterialLink, {
|
||||
linkClasses as materialLinkClasses,
|
||||
} from '@mui/material/Link';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface LinkProps extends MaterialLinkProps {}
|
||||
export interface LinkProps extends MaterialLinkProps {
|
||||
/**
|
||||
* Controls when the link should have an underline.
|
||||
* @default 'hover'
|
||||
*/
|
||||
underline?: MaterialLinkProps['underline'];
|
||||
}
|
||||
|
||||
function Link(
|
||||
{ children, ...props }: LinkProps,
|
||||
{ children, underline = 'hover', ...props }: LinkProps,
|
||||
ref: ForwardedRef<HTMLAnchorElement>,
|
||||
) {
|
||||
return (
|
||||
<MaterialLink ref={ref} {...props}>
|
||||
<MaterialLink underline={underline} ref={ref} {...props}>
|
||||
{children}
|
||||
</MaterialLink>
|
||||
);
|
||||
@@ -18,4 +26,6 @@ function Link(
|
||||
|
||||
Link.displayName = 'NhostLink';
|
||||
|
||||
export { materialLinkClasses as linkClasses };
|
||||
|
||||
export default forwardRef(Link);
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface OptionProps<TValue extends {}>
|
||||
|
||||
const StyledOption = styled(OptionUnstyled)(({ theme }) => ({
|
||||
transition: theme.transitions.create(['background-color']),
|
||||
color: theme.palette.text.primary,
|
||||
[`&.${optionUnstyledClasses.selected}`]: {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark'
|
||||
|
||||
@@ -53,7 +53,7 @@ const StyledListbox = styled('ul')(({ theme }) => ({
|
||||
? `1px solid ${theme.palette.grey[300]}`
|
||||
: 'none',
|
||||
borderWidth: theme.palette.mode === 'dark' ? 1 : 0,
|
||||
borderColor: theme.palette.mode === 'dark' ? 'grey.400' : 'none',
|
||||
borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[400] : 'none',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
|
||||
@@ -19,6 +19,10 @@ export type TextProps<
|
||||
* @default 'primary'
|
||||
*/
|
||||
color?: 'primary' | 'secondary' | 'disabled' | 'error';
|
||||
/**
|
||||
* The component used for the root node.
|
||||
*/
|
||||
component?: D;
|
||||
};
|
||||
|
||||
const textClasses = {
|
||||
|
||||
@@ -4,23 +4,31 @@ import useColorPreference from '@/ui/v2/useColorPreference';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
import { ThemeProvider as MaterialThemeProvider } from '@mui/material/styles';
|
||||
import Head from 'next/head';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
function ThemeProviderContent({ children }: PropsWithChildren<unknown>) {
|
||||
function ThemeProviderContent({
|
||||
children,
|
||||
color: manualColor,
|
||||
}: PropsWithChildren<{ color?: 'light' | 'dark' }>) {
|
||||
const { color } = useColorPreference();
|
||||
const theme = createTheme(color);
|
||||
const theme = createTheme(manualColor || color);
|
||||
|
||||
return (
|
||||
<MaterialThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
'html, body': {
|
||||
backgroundColor: `${theme.palette.background.default} !important`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Head>
|
||||
<meta name="theme-color" content={theme.palette.background.paper} />
|
||||
</Head>
|
||||
|
||||
{children}
|
||||
</MaterialThemeProvider>
|
||||
);
|
||||
@@ -33,17 +41,22 @@ export interface ThemeProviderProps extends PropsWithChildren<unknown> {
|
||||
* @default 'color-mode'
|
||||
*/
|
||||
colorPreferenceStorageKey?: string;
|
||||
/**
|
||||
* Manually set the color preference.
|
||||
*/
|
||||
color?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
function ThemeProvider({
|
||||
children,
|
||||
color,
|
||||
colorPreferenceStorageKey = 'color-preference',
|
||||
}: ThemeProviderProps) {
|
||||
return (
|
||||
<ColorPreferenceProvider
|
||||
colorPreferenceStorageKey={colorPreferenceStorageKey}
|
||||
>
|
||||
<ThemeProviderContent>{children}</ThemeProviderContent>
|
||||
<ThemeProviderContent color={color}>{children}</ThemeProviderContent>
|
||||
</ColorPreferenceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Input from '@/ui/v2/Input';
|
||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import axios from 'axios';
|
||||
import fetch from 'cross-fetch';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -79,28 +79,36 @@ export default function CreateUserForm({
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
axios.post(signUpUrl, {
|
||||
email,
|
||||
password,
|
||||
fetch(signUpUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then(async (res) => {
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (res.status === 409) {
|
||||
setError('email', { message: data?.message });
|
||||
}
|
||||
|
||||
throw new Error(data?.message || 'Something went wrong.');
|
||||
}),
|
||||
{
|
||||
loading: 'Creating user...',
|
||||
success: 'User created successfully.',
|
||||
error: 'An error occurred while trying to create the user.',
|
||||
error: (arg) =>
|
||||
arg?.message
|
||||
? `Error: ${arg.message}`
|
||||
: 'An error occurred while trying to create the user.',
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
if (error.response?.status === 409) {
|
||||
setError('email', {
|
||||
message: error.response.data.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCreateUserFormError(
|
||||
new Error(error.response.data.message || 'Something went wrong.'),
|
||||
);
|
||||
} catch {
|
||||
// Note: Error is already handled by toast.promise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +145,7 @@ export default function CreateUserForm({
|
||||
{createUserFormError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
className="grid items-center justify-between grid-flow-col px-4 py-3"
|
||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
||||
>
|
||||
<span className="text-left">
|
||||
<strong>Error:</strong> {createUserFormError.message}
|
||||
|
||||
@@ -268,7 +268,7 @@ export default function EditUserForm({
|
||||
Created At
|
||||
</InputLabel>
|
||||
<Text className="col-span-3 font-medium">
|
||||
{format(new Date(user.createdAt), 'yyyy-MM-dd hh:mm:ss')}
|
||||
{format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
</Text>
|
||||
|
||||
<InputLabel as="h3" className="col-span-1 self-center ">
|
||||
@@ -276,7 +276,7 @@ export default function EditUserForm({
|
||||
</InputLabel>
|
||||
<Text className="col-span-3 font-medium">
|
||||
{user.lastSeen
|
||||
? `${format(new Date(user.lastSeen), 'yyyy-mm-dd hh:mm:ss')}`
|
||||
? `${format(new Date(user.lastSeen), 'yyyy-MM-dd HH:mm:ss')}`
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -25,10 +25,25 @@ export const UserDataContext = createContext<UserDataContent>({
|
||||
setUserContext: () => {},
|
||||
});
|
||||
|
||||
export function UserDataProvider({ children }: PropsWithChildren<unknown>) {
|
||||
export interface UserDataProviderProps {
|
||||
/**
|
||||
* Initial workspaces to be used in the context.
|
||||
*/
|
||||
initialWorkspaces?: Workspace[];
|
||||
/**
|
||||
* Initial metadata to be used in the context.
|
||||
*/
|
||||
initialMetadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function UserDataProvider({
|
||||
children,
|
||||
initialWorkspaces,
|
||||
initialMetadata,
|
||||
}: PropsWithChildren<UserDataProviderProps>) {
|
||||
const [userContext, setUserContext] = useState({
|
||||
workspaces: [],
|
||||
metadata: {},
|
||||
workspaces: initialWorkspaces || [],
|
||||
metadata: initialMetadata || {},
|
||||
});
|
||||
|
||||
const value = useMemo(
|
||||
|
||||
@@ -31,6 +31,7 @@ test('should throw an error if permission object is incorrectly provided', async
|
||||
action: 'select',
|
||||
role: 'user',
|
||||
mode: 'update',
|
||||
resourceVersion: 1,
|
||||
}),
|
||||
).rejects.toThrowError(
|
||||
new Error(
|
||||
|
||||
@@ -17,6 +17,10 @@ export interface ManagePermissionVariables {
|
||||
* The action to manage the permission for.
|
||||
*/
|
||||
action: DatabaseAction;
|
||||
/**
|
||||
* The resource version of the metadata.
|
||||
*/
|
||||
resourceVersion: number;
|
||||
/**
|
||||
* Permission to insert or update.
|
||||
*/
|
||||
@@ -45,6 +49,7 @@ export default async function managePermission({
|
||||
permission,
|
||||
role,
|
||||
action,
|
||||
resourceVersion,
|
||||
mode = 'update',
|
||||
}: ManagePermissionOptions & ManagePermissionVariables) {
|
||||
if (mode !== 'delete' && !permission) {
|
||||
@@ -87,7 +92,7 @@ export default async function managePermission({
|
||||
args,
|
||||
type: 'bulk',
|
||||
source: dataSource,
|
||||
version: 1,
|
||||
resource_version: resourceVersion,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ export interface ManagePermissionMigrationVariables {
|
||||
* The action to manage the permission for.
|
||||
*/
|
||||
action: DatabaseAction;
|
||||
/**
|
||||
* The resource version of the metadata.
|
||||
*/
|
||||
resourceVersion: number;
|
||||
/**
|
||||
* Permission to insert or update.
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,12 @@ import fetch from 'cross-fetch';
|
||||
|
||||
export interface FetchMetadataOptions
|
||||
extends Omit<MutationOrQueryBaseOptions, 'schema' | 'table'> {}
|
||||
export interface FetchMetadataReturnType extends HasuraMetadataSource {}
|
||||
export interface FetchMetadataReturnType extends Partial<HasuraMetadataSource> {
|
||||
/**
|
||||
* The resource version of the metadata.
|
||||
*/
|
||||
resourceVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Hasura metadata using the Metadata API.
|
||||
@@ -33,8 +38,9 @@ export default async function fetchMetadata({
|
||||
}),
|
||||
});
|
||||
|
||||
const responseData: Record<string, HasuraMetadata> | QueryError =
|
||||
await response.json();
|
||||
const responseData:
|
||||
| { metadata: HasuraMetadata; resource_version: number }
|
||||
| QueryError = await response.json();
|
||||
|
||||
if (!response.ok || 'error' in responseData) {
|
||||
if ('internal' in responseData) {
|
||||
@@ -48,9 +54,11 @@ export default async function fetchMetadata({
|
||||
}
|
||||
}
|
||||
|
||||
const { metadata } = responseData;
|
||||
const { metadata, resource_version: resourceVersion } = responseData;
|
||||
const currentSource =
|
||||
metadata?.sources?.find((source) => source.name === dataSource) || null;
|
||||
|
||||
return (
|
||||
metadata?.sources?.find((source) => source.name === dataSource) || null
|
||||
);
|
||||
return currentSource
|
||||
? { ...currentSource, resourceVersion }
|
||||
: { resourceVersion };
|
||||
}
|
||||
|
||||
@@ -35,15 +35,15 @@ export default function useNotFoundRedirect() {
|
||||
}
|
||||
|
||||
if (noResolvedWorkspace && notIn404Already) {
|
||||
router.push('/404');
|
||||
router.replace('/404');
|
||||
}
|
||||
|
||||
if (noResolvedApplication && notIn404Already) {
|
||||
router.push('/404');
|
||||
router.replace('/404');
|
||||
}
|
||||
|
||||
if (isProjectUsingRDS && inSettingsDatabasePage) {
|
||||
router.push('/404');
|
||||
router.replace('/404');
|
||||
}
|
||||
}, [
|
||||
updating,
|
||||
|
||||
@@ -12,13 +12,11 @@ export function authProtected<P>(Comp: ComponentType<P>) {
|
||||
const { isAuthenticated, isLoading } = useAuthenticationStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (!isAuthenticated && router.pathname === '/') {
|
||||
router.push('/signup');
|
||||
} else if (!isAuthenticated) {
|
||||
router.push('/signin');
|
||||
}
|
||||
if (isLoading || isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/signin');
|
||||
}, [isLoading, isAuthenticated, router]);
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -1,38 +1,67 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Input, { inputClasses } from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { useResetPassword } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import NavLink from 'next/link';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type ResetPasswordFormProps = {
|
||||
email: string;
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string().label('Email').email().required(),
|
||||
});
|
||||
|
||||
function ResetPasswordForm() {
|
||||
const { resetPassword, isSent, isLoading, isError, error } =
|
||||
useResetPassword();
|
||||
export type ResetPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const { register, handleSubmit, getValues } = useForm<ResetPasswordFormProps>(
|
||||
{
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
const StyledInput = styled(Input)({
|
||||
backgroundColor: 'transparent',
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const { resetPassword, error, isSent } = useResetPassword();
|
||||
|
||||
const form = useForm<ResetPasswordFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
);
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ResetPasswordFormProps) => {
|
||||
const { email } = data;
|
||||
const { register, formState, getValues } = form;
|
||||
|
||||
await resetPassword(email);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(
|
||||
error?.message || 'An error occurred while signing in. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
async function handleSubmit({ email }: ResetPasswordFormValues) {
|
||||
try {
|
||||
await resetPassword(email);
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred while signing up. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSent) {
|
||||
return (
|
||||
@@ -44,102 +73,54 @@ function ResetPasswordForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-w-2xl flex-col items-center">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col space-y-3"
|
||||
<>
|
||||
<Text
|
||||
variant="h2"
|
||||
component="h1"
|
||||
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
|
||||
>
|
||||
<Input
|
||||
{...register('email')}
|
||||
autoFocus
|
||||
id="email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
required
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="email"
|
||||
/>
|
||||
Reset Password
|
||||
</Text>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Send Reset Instructions
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{isError && (
|
||||
<div className="my-3">
|
||||
<Text className="font-medium" color="error">
|
||||
Error: {error.message}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex max-w-3xl flex-col">
|
||||
<div className="z-30 mb-8 flex justify-center">
|
||||
<a href="https://nhost.io" tabIndex={-1}>
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? '/assets/brands/light/nhost-with-text.svg'
|
||||
: '/assets/brands/nhost-with-text.svg'
|
||||
}
|
||||
alt="Nhost Logo"
|
||||
width={185}
|
||||
height={64}
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<StyledInput
|
||||
{...register('email')}
|
||||
type="email"
|
||||
id="email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
fullWidth
|
||||
autoFocus
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.email}
|
||||
helperText={formState.errors.email?.message}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="z-30">
|
||||
<Box
|
||||
className="rounded-lg border px-12 py-4"
|
||||
style={{ width: '480px' }}
|
||||
|
||||
<Button
|
||||
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
>
|
||||
<div className="my-4">
|
||||
<div className="mb-4 flex justify-center text-lg font-semibold">
|
||||
Reset your password
|
||||
</div>
|
||||
<ResetPasswordForm />
|
||||
</div>
|
||||
</Box>
|
||||
Send Reset Instructions
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Box>
|
||||
|
||||
<div className="mt-3 flex justify-center">
|
||||
<div className="grid grid-flow-col items-center justify-center gap-1">
|
||||
<Text className="text-sm">Is your password okay?</Text>
|
||||
|
||||
<NavLink href="/signin" passHref>
|
||||
<Link href="signin" underline="hover">
|
||||
Sign in
|
||||
</Link>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute z-0 w-full max-w-[887px]">
|
||||
<Image
|
||||
src="/assets/signup/bg-gradient.svg"
|
||||
alt="Gradient background"
|
||||
width={887}
|
||||
height={620}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Text color="secondary" className="text-center text-base lg:text-lg">
|
||||
Is your password okay?{' '}
|
||||
<NavLink href="/signin/email" color="white" className="font-medium">
|
||||
Sign In
|
||||
</NavLink>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,238 +1,100 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import GithubIcon from '@/components/icons/GithubIcon';
|
||||
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useSignInEmailPassword } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import NavLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
function SignInWithGithub({ setSignInMethod }: any) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
export default function SignUpPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-row items-center space-x-4 rounded-sm+ bg-github px-6 py-3 font-display font-medium text-white transition-all duration-200 ease-in-out disabled:opacity-40"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
nhost.auth.signIn({ provider: 'github' });
|
||||
}}
|
||||
<>
|
||||
<Text
|
||||
variant="h2"
|
||||
component="h1"
|
||||
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
|
||||
>
|
||||
<GithubIcon className="h-6 w-6" />
|
||||
<div>Continue with GitHub</div>
|
||||
</button>
|
||||
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
|
||||
<span>or</span>
|
||||
It's time to build
|
||||
</Text>
|
||||
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<Button
|
||||
variant="borderless"
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={() => setSignInMethod('email')}
|
||||
className="hover:bg-transparent hover:underline"
|
||||
>
|
||||
sign in with email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
|
||||
startIcon={<GithubIcon />}
|
||||
size="large"
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
type SignInFormProps = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
function SignInWithEmail({ setSignInMethod }: any) {
|
||||
const { signInEmailPassword, isLoading, isSuccess, isError, error } =
|
||||
useSignInEmailPassword();
|
||||
|
||||
const form = useForm<SignInFormProps>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
const { register } = form;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
async function onSubmit({ email, password }: SignInFormProps) {
|
||||
await signInEmailPassword(email, password);
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex max-w-2xl flex-col items-center">
|
||||
<FormProvider register={register} {...form}>
|
||||
<Form onSubmit={onSubmit} className="grid w-full grid-flow-row gap-3">
|
||||
<Input
|
||||
{...register('email')}
|
||||
autoFocus
|
||||
id="email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
required
|
||||
fullWidth
|
||||
inputProps={{
|
||||
min: 2,
|
||||
max: 128,
|
||||
}}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="email"
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('password')}
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
fullWidth
|
||||
label={
|
||||
<span className="grid grid-flow-col justify-between">
|
||||
<span>Password</span>
|
||||
|
||||
<NavLink href="/reset-password" passHref>
|
||||
<Link
|
||||
href="reset-password"
|
||||
tabIndex={-1}
|
||||
className="text-xs"
|
||||
underline="hover"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</NavLink>
|
||||
</span>
|
||||
try {
|
||||
await nhost.auth.signIn({ provider: 'github' });
|
||||
} catch {
|
||||
toast.error(
|
||||
`An error occurred while trying to sign in using GitHub. Please try again later.`,
|
||||
getToastStyleProps(),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="password"
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
}}
|
||||
>
|
||||
Continue with GitHub
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Sign In
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
{isError && (
|
||||
<Text className="my-3 font-medium" color="error">
|
||||
Error: {error.message}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
|
||||
<span>or</span>
|
||||
<Button
|
||||
variant="borderless"
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={() => setSignInMethod('github')}
|
||||
className="hover:bg-transparent hover:underline"
|
||||
className="!text-white hover:!bg-white hover:!bg-opacity-10 focus:!bg-white focus:!bg-opacity-10"
|
||||
size="large"
|
||||
href="/signin/email"
|
||||
LinkComponent={NavLink}
|
||||
>
|
||||
sign in with GitHub
|
||||
Continue with Email
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Divider className="!my-2" />
|
||||
|
||||
<Text color="secondary" className="text-center text-sm">
|
||||
By clicking continue, you agree to our{' '}
|
||||
<NavLink
|
||||
href="https://nhost.io/legal/terms-of-service"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold"
|
||||
color="white"
|
||||
>
|
||||
Terms of Service
|
||||
</NavLink>{' '}
|
||||
and{' '}
|
||||
<NavLink
|
||||
href="https://nhost.io/legal/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold"
|
||||
color="white"
|
||||
>
|
||||
Privacy Policy
|
||||
</NavLink>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text color="secondary" className="text-center lg:text-lg">
|
||||
Don't have an account?{' '}
|
||||
<NavLink href="/signup" color="white" className="font-medium">
|
||||
Sign Up
|
||||
</NavLink>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SignInController() {
|
||||
const [signInMethod, setSignInMethod] = useState('github');
|
||||
|
||||
if (signInMethod === 'github') {
|
||||
return <SignInWithGithub setSignInMethod={setSignInMethod} />;
|
||||
}
|
||||
|
||||
if (signInMethod === 'email') {
|
||||
return <SignInWithEmail setSignInMethod={setSignInMethod} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function SignInPage() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="flex max-w-3xl flex-col">
|
||||
<div className="z-30 mb-8 flex justify-center">
|
||||
<a href="https://nhost.io" tabIndex={-1}>
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? '/assets/brands/light/nhost-with-text.svg'
|
||||
: '/assets/brands/nhost-with-text.svg'
|
||||
}
|
||||
alt="Nhost Logo"
|
||||
width={185}
|
||||
height={64}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="z-10">
|
||||
<Box
|
||||
className="grid grid-flow-row gap-4 rounded-lg border px-12 py-8"
|
||||
style={{ width: '480px' }}
|
||||
>
|
||||
<Text variant="h1" className="text-center text-lg font-semibold">
|
||||
Sign in to Nhost
|
||||
</Text>
|
||||
|
||||
<SignInController />
|
||||
</Box>
|
||||
|
||||
<div className="mt-3 flex justify-center">
|
||||
<div className="grid grid-flow-col items-center justify-center gap-1">
|
||||
<Text className="text-sm">Don't have an account?</Text>
|
||||
|
||||
<NavLink href="/signup" passHref>
|
||||
<Link href="signup" underline="hover">
|
||||
Sign up
|
||||
</Link>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute z-0 w-full max-w-[887px]">
|
||||
<Image
|
||||
src="/assets/signup/bg-gradient.svg"
|
||||
alt="Gradient background"
|
||||
width={887}
|
||||
height={620}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SignInPage.getLayout = function getLayout(page: ReactElement) {
|
||||
SignUpPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
|
||||
};
|
||||
|
||||
155
dashboard/src/pages/signin/email.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input, { inputClasses } from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { useSignInEmailPassword } from '@nhost/nextjs';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string().label('Email').email().required(),
|
||||
password: Yup.string().label('Password').required(),
|
||||
});
|
||||
|
||||
export type EmailSignUpFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const StyledInput = styled(Input)({
|
||||
backgroundColor: 'transparent',
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
});
|
||||
|
||||
export default function EmailSignUpPage() {
|
||||
const { signInEmailPassword, error } = useSignInEmailPassword();
|
||||
|
||||
const form = useForm<EmailSignUpFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const { register, formState } = form;
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(
|
||||
error?.message || 'An error occurred while signing in. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
async function handleSubmit({ email, password }: EmailSignUpFormValues) {
|
||||
try {
|
||||
await signInEmailPassword(email, password);
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred while signing in. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
variant="h2"
|
||||
component="h1"
|
||||
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
|
||||
>
|
||||
Sign In
|
||||
</Text>
|
||||
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<StyledInput
|
||||
{...register('email')}
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="email"
|
||||
label="Email"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={!!formState.errors.email}
|
||||
helperText={formState.errors.email?.message}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<StyledInput
|
||||
{...register('password')}
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="password"
|
||||
label="Password"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={!!formState.errors.password}
|
||||
helperText={formState.errors.password?.message}
|
||||
/>
|
||||
|
||||
<NavLink
|
||||
href="/reset-password"
|
||||
color="white"
|
||||
className="justify-self-start font-semibold"
|
||||
>
|
||||
Forgot password?
|
||||
</NavLink>
|
||||
|
||||
<Button
|
||||
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
|
||||
size="large"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<Text color="secondary" className="text-center">
|
||||
or{' '}
|
||||
<NavLink color="white" className="font-semibold" href="/signin">
|
||||
sign in with GitHub
|
||||
</NavLink>
|
||||
</Text>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Box>
|
||||
|
||||
<Text color="secondary" className="text-center text-base lg:text-lg">
|
||||
Don't have an account?{' '}
|
||||
<NavLink href="/signup" color="white">
|
||||
Sign Up
|
||||
</NavLink>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EmailSignUpPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
|
||||
};
|
||||
@@ -1,342 +1,214 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import GithubIcon from '@/components/icons/GithubIcon';
|
||||
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Input, { inputClasses } from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { useSignUpEmailPassword } from '@nhost/nextjs';
|
||||
import kebabCase from 'just-kebab-case';
|
||||
import Image from 'next/image';
|
||||
import NavLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const sellingPoints = [
|
||||
'Awesome Free Plan',
|
||||
'Postgres Database',
|
||||
'Instant GraphQL API with Hasura',
|
||||
'Authentication and Storage',
|
||||
'TypeScript Client',
|
||||
'Serverless Functions',
|
||||
'Push code to deploy with our GitHub Integration',
|
||||
];
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string().label('Email').email().required(),
|
||||
password: Yup.string().label('Password').required(),
|
||||
displayName: Yup.string().label('Name').required(),
|
||||
});
|
||||
|
||||
const companies = [
|
||||
'Revtron',
|
||||
'HyperLab',
|
||||
'Orthopy',
|
||||
'Celsia',
|
||||
'ServeHub',
|
||||
'Teamtailor',
|
||||
];
|
||||
export type SignUpFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
function SignUpWithGithub({ setSignUpMethod }: any) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const StyledInput = styled(Input)({
|
||||
backgroundColor: 'transparent',
|
||||
[`& .${inputClasses.input}`]: {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-row items-center space-x-6 rounded-sm+ bg-github px-6 py-3 font-display font-medium text-white transition-all duration-200 ease-in-out disabled:opacity-40"
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
nhost.auth.signIn({ provider: 'github' });
|
||||
}}
|
||||
>
|
||||
<GithubIcon className="h-6 w-6 text-white" />
|
||||
<div>Sign Up with GitHub</div>
|
||||
</button>
|
||||
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
|
||||
<span>or</span>
|
||||
<Button
|
||||
variant="borderless"
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={() => setSignUpMethod('email')}
|
||||
className="hover:bg-transparent hover:underline"
|
||||
>
|
||||
sign up with email
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function SignUpPage() {
|
||||
const { signUpEmailPassword, error } = useSignUpEmailPassword();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
type SignUpFormProps = {
|
||||
displayName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
function SignUpWithEmail({ setSignUpMethod }: any) {
|
||||
const { signUpEmailPassword, isLoading, isSuccess, isError, error } =
|
||||
useSignUpEmailPassword();
|
||||
|
||||
const form = useForm<SignUpFormProps>({
|
||||
const form = useForm<SignUpFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
displayName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const { register } = form;
|
||||
const { register, formState } = form;
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function onSubmit({ email, password, displayName }: SignUpFormProps) {
|
||||
await signUpEmailPassword(email, password, {
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
toast.error(
|
||||
error?.message || 'An error occurred while signing up. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
if (isSuccess) {
|
||||
router.push('/');
|
||||
async function handleSubmit({
|
||||
email,
|
||||
password,
|
||||
displayName,
|
||||
}: SignUpFormValues) {
|
||||
try {
|
||||
await signUpEmailPassword(email, password, { displayName });
|
||||
} catch {
|
||||
toast.error(
|
||||
'An error occurred while signing up. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row items-center justify-items-center gap-2">
|
||||
<Text variant="h1" className="text-lg font-semibold">
|
||||
Sign Up with Email
|
||||
<>
|
||||
<Text
|
||||
variant="h2"
|
||||
component="h1"
|
||||
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
|
||||
>
|
||||
Sign Up
|
||||
</Text>
|
||||
|
||||
<FormProvider register={register} {...form}>
|
||||
<Form onSubmit={onSubmit} className="grid w-full grid-flow-row gap-3">
|
||||
<Input
|
||||
{...register('displayName')}
|
||||
id="displayName"
|
||||
placeholder="Name"
|
||||
required
|
||||
inputProps={{
|
||||
min: 2,
|
||||
max: 128,
|
||||
}}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="text"
|
||||
autoFocus
|
||||
label="Name"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('email')}
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
required
|
||||
inputProps={{
|
||||
min: 2,
|
||||
max: 128,
|
||||
}}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="email"
|
||||
label="Email"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('password')}
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
inputProps={{
|
||||
min: 2,
|
||||
max: 128,
|
||||
}}
|
||||
spellCheck="false"
|
||||
autoCapitalize="none"
|
||||
type="password"
|
||||
label="Password"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Sign Up
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
{isError && (
|
||||
<Text className="font-medium" color="error">
|
||||
Error: {error.message}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
|
||||
<span>or</span>
|
||||
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
|
||||
<Button
|
||||
variant="borderless"
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={() => setSignUpMethod('github')}
|
||||
className="hover:bg-transparent hover:underline"
|
||||
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
|
||||
startIcon={<GithubIcon />}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
size="large"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await nhost.auth.signIn({ provider: 'github' });
|
||||
} catch {
|
||||
toast.error(
|
||||
`An error occurred while trying to sign up using GitHub. Please try again.`,
|
||||
getToastStyleProps(),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
sign up with GitHub
|
||||
Sign Up with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignUpController() {
|
||||
const [signUpMethod, setSignUpMethod] = useState('github');
|
||||
<div className="relative py-2">
|
||||
<Text
|
||||
className="absolute left-0 right-0 top-1/2 mx-auto w-12 -translate-y-1/2 bg-black px-2 text-center text-sm"
|
||||
color="disabled"
|
||||
>
|
||||
OR
|
||||
</Text>
|
||||
|
||||
if (signUpMethod === 'github') {
|
||||
return <SignUpWithGithub setSignUpMethod={setSignUpMethod} />;
|
||||
}
|
||||
if (signUpMethod === 'email') {
|
||||
return <SignUpWithEmail setSignUpMethod={setSignUpMethod} />;
|
||||
}
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function SignUpPage() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="grid max-w-6xl grid-cols-1 gap-x-20 gap-y-14 px-5 md:grid-cols-2">
|
||||
<div className="flex items-center justify-center md:order-1">
|
||||
<div className="z-30">
|
||||
<div className="block md:hidden">
|
||||
<div className="mb-5">
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? '/assets/brands/light/nhost-with-text.svg'
|
||||
: '/assets/brands/nhost-with-text.svg'
|
||||
}
|
||||
alt="Nhost Logo"
|
||||
width={185}
|
||||
height={64}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 text-3xl font-semibold">
|
||||
Build the App of Your Dreams
|
||||
</div>
|
||||
</div>
|
||||
<Box className="rounded-lg border px-12 py-4">
|
||||
<div className="my-4">
|
||||
<SignUpController />
|
||||
</div>
|
||||
<Text className="mt-4 text-center text-xs" color="secondary">
|
||||
By signing up, you agree to our{' '}
|
||||
<Link
|
||||
href="https://nhost.io/legal/terms-of-service"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link
|
||||
href="https://nhost.io/legal/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Box>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<div className="grid grid-flow-col items-center justify-center gap-1">
|
||||
<Text className="text-sm">Already have an account?</Text>
|
||||
|
||||
<NavLink href="/signin" passHref>
|
||||
<Link href="signin" underline="hover">
|
||||
Sign in
|
||||
</Link>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute w-full max-w-[887px]">
|
||||
<Image
|
||||
src="/assets/signup/bg-gradient.svg"
|
||||
alt="Gradient background"
|
||||
width={887}
|
||||
height={620}
|
||||
layout="responsive"
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="grid grid-flow-row gap-4 bg-transparent"
|
||||
>
|
||||
<StyledInput
|
||||
{...register('displayName')}
|
||||
id="displayName"
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
fullWidth
|
||||
autoFocus
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.email}
|
||||
helperText={formState.errors.email?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="hidden md:block">
|
||||
<div className="mb-10">
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? '/assets/brands/light/nhost-with-text.svg'
|
||||
: '/assets/brands/nhost-with-text.svg'
|
||||
}
|
||||
alt="Nhost Logo"
|
||||
width={185}
|
||||
height={64}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 text-2xl font-semibold">
|
||||
Build the App of Your Dreams
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 flex flex-col space-y-3">
|
||||
{sellingPoints.map((sellingPoint) => (
|
||||
<div key={sellingPoint} className="flex items-center space-x-2">
|
||||
<Image
|
||||
src="/assets/signup/CircleWavyCheck.svg"
|
||||
alt="Check"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
|
||||
<Text className="!text-xl" color="secondary">
|
||||
{sellingPoint}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Box
|
||||
className="my-14 h-2 opacity-20"
|
||||
sx={{ backgroundColor: 'primary.main' }}
|
||||
/>
|
||||
<div className="my-4 grid grid-cols-3 items-center gap-x-6 gap-y-6 opacity-40">
|
||||
{companies.map((company) => (
|
||||
<div key={company} className="h-[25px] w-[150px]">
|
||||
<Image
|
||||
src={
|
||||
theme.palette.mode === 'dark'
|
||||
? `/assets/brands/light/${kebabCase(company)}.svg`
|
||||
: `/assets/brands/${kebabCase(company)}.svg`
|
||||
}
|
||||
alt={`Logo of ${company}`}
|
||||
width={150}
|
||||
height={25}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<StyledInput
|
||||
{...register('email')}
|
||||
type="email"
|
||||
id="email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.email}
|
||||
helperText={formState.errors.email?.message}
|
||||
/>
|
||||
|
||||
<StyledInput
|
||||
{...register('password')}
|
||||
type="password"
|
||||
id="password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!formState.errors.password}
|
||||
helperText={formState.errors.password?.message}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Divider className="!my-2" />
|
||||
|
||||
<Text color="secondary" className="text-center text-sm">
|
||||
By signing up, you agree to our{' '}
|
||||
<NavLink
|
||||
href="https://nhost.io/legal/terms-of-service"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold"
|
||||
color="white"
|
||||
>
|
||||
Terms of Service
|
||||
</NavLink>{' '}
|
||||
and{' '}
|
||||
<NavLink
|
||||
href="https://nhost.io/legal/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold"
|
||||
color="white"
|
||||
>
|
||||
Privacy Policy
|
||||
</NavLink>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Text color="secondary" className="text-center text-base lg:text-lg">
|
||||
Already have an account?{' '}
|
||||
<NavLink href="/signin" color="white" className="font-medium">
|
||||
Sign In
|
||||
</NavLink>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#__next {
|
||||
background-color: #151a22;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
#__next {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-show-main {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface HasuraMetadataPermission {
|
||||
allow_aggregations: boolean;
|
||||
query_root_fields: string[];
|
||||
subscription_root_fields: string[];
|
||||
computed_fields: string[];
|
||||
set: Record<string, any>;
|
||||
backend_only: boolean;
|
||||
}>;
|
||||
|
||||
@@ -4,7 +4,11 @@ import slugify from 'slugify';
|
||||
import { LOCAL_BACKEND_URL } from './env';
|
||||
import type { DeploymentRowFragment } from './__generated__/graphql';
|
||||
|
||||
export function getLastLiveDeployment(deployments: DeploymentRowFragment[]) {
|
||||
export function getLastLiveDeployment(deployments?: DeploymentRowFragment[]) {
|
||||
if (!deployments) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
deployments.find((deployment) => deployment.deploymentStatus === 'DEPLOYED')
|
||||
?.id || ''
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ManagedUIContext } from '@/context/UIContext';
|
||||
import { WorkspaceProvider } from '@/context/workspace-context';
|
||||
import { UserDataProvider } from '@/context/workspace1-context';
|
||||
import createTheme from '@/ui/v2/createTheme';
|
||||
import { createHttpLink } from '@apollo/client';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { NhostProvider } from '@nhost/nextjs';
|
||||
@@ -33,7 +34,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
global.fetch = fetch;
|
||||
|
||||
const mockRouter: NextRouter = {
|
||||
export const mockRouter: NextRouter = {
|
||||
basePath: '',
|
||||
pathname: '/',
|
||||
route: '/',
|
||||
@@ -65,7 +66,12 @@ function Providers({ children }: PropsWithChildren<{}>) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider value={emotionCache}>
|
||||
<NhostProvider nhost={nhost}>
|
||||
<NhostApolloProvider nhost={nhost}>
|
||||
<NhostApolloProvider
|
||||
nhost={nhost}
|
||||
link={createHttpLink({
|
||||
uri: 'http://localhost:1337/v1/graphql',
|
||||
})}
|
||||
>
|
||||
<WorkspaceProvider>
|
||||
<UserDataProvider>
|
||||
<ManagedUIContext>
|
||||
|
||||
@@ -111,10 +111,10 @@ module.exports = {
|
||||
},
|
||||
fontSize: {
|
||||
'xs-': ['0.6875rem', '0.875rem'],
|
||||
'sm-': '13px',
|
||||
'sm-': ['0.8125rem', '1rem'],
|
||||
'sm+': ['0.9375rem', '1.25rem'],
|
||||
'2.5xl': '26px',
|
||||
'5.5xl': '56px',
|
||||
'3.5xl': ['2rem', '2.5rem'],
|
||||
'4.5xl': ['2.5rem', '2.5rem'],
|
||||
},
|
||||
maxWidth: {
|
||||
2: '0.5rem',
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 0.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bf1e4071: chore(deps): bump `react` to v18
|
||||
- 8be094be: fix(deps): update docusaurus monorepo to v2.3.1
|
||||
|
||||
## 0.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -5,6 +5,9 @@ sidebar_label: 'Overview'
|
||||
image: /img/og/graphql.png
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
A GraphQL API is automatically and instantly available based on the tables and columns in your [database](/database).
|
||||
|
||||
The GraphQL API has support for inserting, selecting, updating, and deleting data, which usually accounts for 80% of all API operations you need.
|
||||
@@ -21,11 +24,11 @@ Building your GraphQL API is a lot of work, but with Nhost it's easy because eve
|
||||
|
||||
## Endpoint
|
||||
|
||||
The GraphQL API is available at `https://[subdomain].nhost.run/v1/graphql` When using the [CLI](/cli) the GraphQL API is available at `http://localhost:1337/v1/graphql`.
|
||||
The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/v1` When using the [CLI](/cli) the GraphQL API is available at `http://localhost:1337/v1/graphql`.
|
||||
|
||||
## GraphQL Clients for JavaScript
|
||||
|
||||
The Nhost JavaScript client comes with a simple [GraphQL client](/reference/javascript/nhost-js/graphql) that works well for the backend or simple applications.
|
||||
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/graphql) that works well for the backend or simple applications.
|
||||
|
||||
When building more complex frontend applications, we recommend using a more advanced GraphQL client such as:
|
||||
|
||||
@@ -38,7 +41,7 @@ When building more complex frontend applications, we recommend using a more adva
|
||||
|
||||
## Queries
|
||||
|
||||
A GraphQL query is used to fetch data from the database.
|
||||
A query is used to fetch data from the GraphQL API.
|
||||
|
||||
:::tip
|
||||
The [Queries documentation from Hasura](https://hasura.io/docs/latest/graphql/core/databases/postgres/queries/index/) is applicable since we're using Hasura's GraphQL Engine for your project.
|
||||
@@ -46,7 +49,10 @@ The [Queries documentation from Hasura](https://hasura.io/docs/latest/graphql/co
|
||||
|
||||
**Example:** A GraphQL query to select `title`, `body`, and `isCompleted` for every row in the `todos` table.
|
||||
|
||||
```graphql title=GraphQL
|
||||
<Tabs >
|
||||
<TabItem value="request" label="Request" default>
|
||||
|
||||
```graphql
|
||||
query GetTodos {
|
||||
todos {
|
||||
title
|
||||
@@ -56,9 +62,10 @@ query GetTodos {
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
</TabItem>
|
||||
<TabItem value="response" label="Response">
|
||||
|
||||
```json title=Response
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"todos": [
|
||||
@@ -72,13 +79,19 @@ query GetTodos {
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Filtering and sorting
|
||||
|
||||
More complex queries utilize filters, limits, sorting, and aggregation.
|
||||
|
||||
Here's an example of a more complex GraphQL query that selects all items in the `todos` table that are not completed, with the total number of comments and the last five comments:
|
||||
|
||||
```graphql title=GraphQL
|
||||
<Tabs >
|
||||
<TabItem value="request" label="Request" default>
|
||||
|
||||
```graphql
|
||||
query GetTodosWithLatestComments {
|
||||
todos(where: { isCompleted: { _eq: false } }) {
|
||||
title
|
||||
@@ -97,7 +110,10 @@ query GetTodosWithLatestComments {
|
||||
}
|
||||
```
|
||||
|
||||
```json title=Response
|
||||
</TabItem>
|
||||
<TabItem value="response" label="Response">
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"todos": [
|
||||
@@ -127,6 +143,9 @@ query GetTodosWithLatestComments {
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Mutations
|
||||
|
||||
A GraphQL mutation is used to insert, upsert, update, or delete data.
|
||||
@@ -139,7 +158,10 @@ The [Mutations documentation from Hasura](https://hasura.io/docs/latest/graphql/
|
||||
|
||||
**Example:** A GraphQL mutation to insert data:
|
||||
|
||||
```graphql title=GraphQL
|
||||
<Tabs >
|
||||
<TabItem value="request" label="Request" default>
|
||||
|
||||
```graphql
|
||||
mutation InsertTodo {
|
||||
insert_todos(
|
||||
objects: [{ title: "Delete Firebase account", body: "Migrate to Nhost", isCompleted: false }]
|
||||
@@ -154,7 +176,10 @@ mutation InsertTodo {
|
||||
}
|
||||
```
|
||||
|
||||
```json title=Reponse
|
||||
</TabItem>
|
||||
<TabItem value="response" label="Response">
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"insert_todos": [
|
||||
@@ -169,6 +194,9 @@ mutation InsertTodo {
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Insert Multiple Rows
|
||||
|
||||
Use an array of objects to insert multiple rows at the same time.
|
||||
@@ -303,9 +331,7 @@ mutation DeleteDoneTodos {
|
||||
}
|
||||
```
|
||||
|
||||
If you have set up foreign keys which will restrict a delete violation, you will get an error and will not be able to delete the data until all violations are solved. The simplest way to solve this is by set `On Delete Violation` to `CASCADE` when you set up a foreign Key.
|
||||
|
||||
---
|
||||
If you have set up foreign keys which will restrict a delete violation, you will get an error and will not be able to delete the data until all violations are solved. The simplest way to solve this is by set `On Delete Violation` to `CASCADE` when you set up a foreign key.
|
||||
|
||||
## Subscriptions
|
||||
|
||||
|
||||
@@ -8,35 +8,19 @@ The GraphQL API is protected by a role-based permission system.
|
||||
|
||||
For each **role**, you create **rules** for the **select**, **insert**, **update**, and **delete** operations.
|
||||
|
||||
**Example:** Let's say you have a `posts` table, and you want users to only access their own posts. This is how you would do it:
|
||||
**Example:** Let's say you have a `posts` table with `id` `title` and `user_id` columns, and you want users to only access their own posts. This is how you would do it:
|
||||
|
||||
```sql title="Posts Table"
|
||||
CREATE TABLE "public"."posts" (
|
||||
"id" serial NOT NULL PRIMARY KEY,
|
||||
"title" text NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
);
|
||||
```
|
||||

|
||||
|
||||
```json title="Hasura Permission Rule"
|
||||
{
|
||||
"user_id": {
|
||||
"_eq": "X-Hasura-User-Id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The rule above make it so users can only select posts where the value of `user_id` is equal (`_eq`) to their user ID (`x-hasura-user-id`).
|
||||
The permission above makes sure users can only select their own posts, because the value of `user_id` must be equal (`_eq`) to the authenticated user's ID (`x-hasura-user-id`).
|
||||
|
||||
## What is `x-hasura-user-id`?
|
||||
|
||||
`x-hasura-user-id` is a permission variable that is used to create permission rules in Hasura. The permission variable comes from the [access token](/authentication/tokens#access-token) that authenticated users have.
|
||||
|
||||
The `x-hasura-user-id` permission variable is always available for all authenticated users. You can add [permission variables](#permission-variables) to create more complex permission rules unique to your project.
|
||||
`x-hasura-user-id` is a permission variable that is used to create permission rules. The permission variable comes from the [access token](/authentication/tokens#access-token) that all authenticated users have. You can add [custom permission variables](#permission-variables) to create more complex permission rules unique to your project.
|
||||
|
||||
## Permission Variables
|
||||
|
||||
You can add permission variables in the Nhost Dashboard under **Settings -> Roles and permissions**. These permission variables are automatically added to users' [access tokens](/authentication/tokens#access-token). This way, permission variables are available when creating permissions for your GraphQL API in the Hasura Console.
|
||||
You can add permission variables in the Nhost Dashboard under **Settings -> Roles and permissions -> Permission Variables**. These permission variables are automatically added to users' [access tokens](/authentication/tokens#access-token). This way, permission variables are available when creating permissions for your GraphQL API.
|
||||
|
||||

|
||||
|
||||
@@ -137,37 +121,28 @@ GraphQL requests from unauthenticated users resolve permissions using the `publi
|
||||
Here is a popular approach for insert permission for authenticated users.
|
||||
|
||||
1. At the top of the page, click **"insert"** on the **"user"** role.
|
||||
2. Select **"Without any checks"**.
|
||||
3. Select the columns you want to allow users to insert.
|
||||
|
||||
In our example, we only mark `title`, because the other columns should not be inserted by the user.
|
||||
|
||||
We also want every new record's `user_id` value to be set to the ID of the user making the request. We can tell Hasura to do this using **Column presets**.
|
||||
|
||||
4. Under **Column presets**, set `user_id` to `x-hasura-user-id`.
|
||||
1. Select **"Without any checks"**.
|
||||
1. Select the columns you want to allow users to insert. In our example, we only mark `title`, because that's the only column that should be inserted by the user. The `id` is automatically generated by the database and `user_id` is set using a column preset.
|
||||
1. Under **Column presets**, set `user_id` to `x-hasura-user-id`. This way, every new record's `user_id` value is set to the ID of the user making the request.
|
||||
|
||||
Now, authenticated users are allowed to insert posts. Users are allowed to add a title when inserting a post. The post's `id` is automatically generated by the database and the `user_id` is automatically set to the user's id using the `user_id = x-hasura-user-id` column preset.
|
||||
|
||||
## Select, Update and Delete Permissions
|
||||
|
||||
Select, update, and delete permissions usually follows the same pattern. Here's an example of how to add select permissions:
|
||||
Select, update, and delete permissions usually follow the same pattern. Here's an example of how to add select permissions:
|
||||
|
||||

|
||||

|
||||
|
||||
One of the most common permission requirements is that authenticated users should only be able to read their own data. This is how to do that:
|
||||
|
||||
1. Go to **Hasura Console**
|
||||
1. Select your table and open the **Permissions** tab
|
||||
1. At the top of the page, click **"select"** on the **"user"** role.
|
||||
1. Go to the **Database** section in the Nhost Dashboard.
|
||||
1. In the context menu of the table you want to edit, click on **Edit Permissions**.
|
||||
1. Click on the **role** and **operation** you want to set.
|
||||
1. Select **"With custom check"** to create a new rule
|
||||
1. Enter `user_id`, `_eq` and `x-hasura-user-id` into the rule form.
|
||||
|
||||
This means that in order for users to read data, the user ID value in the database row must be the same as the user ID in the access token.
|
||||
|
||||
To further refine this rule, do the following:
|
||||
|
||||
1. Limit the number of rows to 100 (or some other relevant number).
|
||||
1. Select the columns you want the user to be able to read. In our case, we'll allow the user to read all columns.
|
||||
1. Enter `user_id`, `_eq` and `x-hasura-user-id` into the rule form. This means that in order for users to read data, the user ID value in the database row must be the same as the user ID in the access token.
|
||||
1. **Limit the number of rows** to 100 (or some other relevant number).
|
||||
1. Select the **columns** you want the user to be able to read. In our case, we'll allow the user to read all columns.
|
||||
1. Click **Save**.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -9,10 +9,16 @@ Initialize a local Nhost project.
|
||||
nhost init
|
||||
```
|
||||
|
||||
If you already have a Nhost project in Nhost Cloud you can use that project as a starting point by appending `--remote` to the command.
|
||||
If you have an existing Nhost project in Nhost Cloud that you want to use as a starting point for local development and for the [Git-based workflow](/platform/git), run `nhost init --remote`.
|
||||
|
||||
```
|
||||
nhost init --remote
|
||||
```
|
||||
The `nhost init --remote` command does the following:
|
||||
|
||||
This will reset the local database migrations and Hasura metadata, and pull the database migrations and Hasura metadata from the Nhost Cloud project locally for you to use as a starting point. While doing `nhost init --remote` it's also recommended to setup [Git](/platform/git) to activate automated deplouments on `git push` for your Nhost project.
|
||||
- Creates a new local Nhost project.
|
||||
- Pulls the database migrations and Hasura metadata from the Nhost Cloud project.
|
||||
- Resets the remote Nhost Cloud project's database migrations.
|
||||
|
||||
:::warning
|
||||
|
||||
The `nhost init --remote` command should only be run **once**. Running it multiple times will reset the remote Nhost Cloud project's database migrations which can cause migration conflict issues in your development team.
|
||||
|
||||
:::
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 'list'
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
List all your Nhost projects in Nhost Cloud.
|
||||
List projects in Nhost Cloud.
|
||||
|
||||
```bash
|
||||
nhost list
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 'logs'
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
Output logs of any service container
|
||||
View logs of all services.
|
||||
|
||||
```bash
|
||||
nhost logs
|
||||
|
||||
@@ -3,10 +3,10 @@ title: 'up'
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
Launch the development environment for your project. Once the environment is up, the command will:
|
||||
To launch the development environment for your project, use the command `nhost up`. Once the environment is running, this command will
|
||||
|
||||
- Apply database migrations.
|
||||
- Apply the Hasura metadata.
|
||||
- Apply Hasura metadata.
|
||||
|
||||
```bash
|
||||
nhost up
|
||||
|
||||
@@ -10,8 +10,8 @@ In this section:
|
||||
- [Overview](/reference/javascript)
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
### React
|
||||
|
||||
|
||||
60
docs/docs/reference/javascript/functions/content/01-call.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: call()
|
||||
sidebar_label: call()
|
||||
slug: /reference/javascript/functions/call
|
||||
description: Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L55
|
||||
---
|
||||
|
||||
# `call()`
|
||||
|
||||
## Overload 1 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
:::caution Deprecated
|
||||
Axios will be replaced by cross-fetch in the near future. Only the headers configuration will be kept.
|
||||
:::
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">optional</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>AxiosRequestConfig<any> & { useAxios: "true" } & [`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "true" }</code>
|
||||
|
||||
---
|
||||
|
||||
## Overload 2 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
```ts
|
||||
await nhost.functions.call('send-welcome-email', {
|
||||
email: 'joe@example.com',
|
||||
name: 'Joe Doe'
|
||||
})
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">required</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>[`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "false" }</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: setAccessToken()
|
||||
sidebar_label: setAccessToken()
|
||||
slug: /reference/javascript/functions/set-access-token
|
||||
description: Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L155
|
||||
---
|
||||
|
||||
# `setAccessToken()`
|
||||
|
||||
Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
|
||||
```ts
|
||||
nhost.functions.setAccessToken('some-access-token')
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>undefined | string</code>
|
||||
|
||||
---
|
||||
22
docs/docs/reference/javascript/functions/index.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: NhostFunctionsClient
|
||||
sidebar_label: Functions
|
||||
description: No description provided.
|
||||
slug: /reference/javascript/functions
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/docs/docs/reference/javascript/functions/index.mdx
|
||||
---
|
||||
|
||||
# `NhostFunctionsClient`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`NhostFunctionsConstructorParams`](/reference/javascript/functions/types/nhost-functions-constructor-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :--------------------------------------------------------------------------------------------- | :------------------ | :------: | :---------------------------------------------------------------------------------------- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>url</span> | <code>string</code> | ✔️ | Serverless Functions endpoint. |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>adminSecret</span> | <code>string</code> | | Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests. |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: NhostFunctionCallConfig
|
||||
sidebar_label: NhostFunctionCallConfig
|
||||
description: Subset of RequestInit parameters that are supported by the functions client
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L41
|
||||
---
|
||||
|
||||
# `NhostFunctionCallConfig`
|
||||
|
||||
Subset of RequestInit parameters that are supported by the functions client
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">headers</span>** <span className="optional-status">optional</span> <code>Record<string, string></code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: NhostFunctionCallResponse
|
||||
sidebar_label: NhostFunctionCallResponse
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L15
|
||||
---
|
||||
|
||||
# `NhostFunctionCallResponse`
|
||||
|
||||
```ts
|
||||
type NhostFunctionCallResponse =
|
||||
| { res: { data: T; status: number; statusText: string }; error: null }
|
||||
| { res: null; error: ErrorPayload }
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: NhostFunctionsConstructorParams
|
||||
sidebar_label: NhostFunctionsConstructorParams
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L4
|
||||
---
|
||||
|
||||
# `NhostFunctionsConstructorParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
Serverless Functions endpoint.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">adminSecret</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests.
|
||||
|
||||
---
|
||||
@@ -10,12 +10,12 @@ The Nhost JavaScript client is the primary way of interacting with your Nhost pr
|
||||
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the the Nhost client together with GraphQL:
|
||||
Install the Nhost client together with GraphQL:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="npm" label="npm" default>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
@@ -16,21 +16,21 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "^4.9.1",
|
||||
"@docusaurus/core": "2.2.0",
|
||||
"@docusaurus/plugin-sitemap": "2.2.0",
|
||||
"@docusaurus/preset-classic": "2.2.0",
|
||||
"@docusaurus/core": "2.3.1",
|
||||
"@docusaurus/plugin-sitemap": "2.3.1",
|
||||
"@docusaurus/preset-classic": "2.3.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-plugin-image-zoom": "^0.1.1",
|
||||
"mdx-mermaid": "^1.3.2",
|
||||
"mermaid": "^9.0.0",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"unist-util-visit": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.2.0",
|
||||
"@docusaurus/module-type-aliases": "2.3.1",
|
||||
"@tsconfig/docusaurus": "^1.0.6",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
|
||||
@@ -111,12 +111,12 @@ const sidebars = {
|
||||
label: 'Functions',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/index'
|
||||
id: 'reference/javascript/functions/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/content'
|
||||
dirName: 'reference/javascript/functions/content'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,12 +125,12 @@ const sidebars = {
|
||||
label: 'GraphQL',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/index'
|
||||
id: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/content'
|
||||
dirName: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/content'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||