Compare commits

..

145 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
1c59c363ee Merge pull request #2328 from nhost/changeset-release/main
chore: update versions
2023-10-24 11:20:22 +01:00
github-actions[bot]
1d99f26fec chore: update versions 2023-10-24 09:59:28 +00:00
Hassan Ben Jobrane
49edb0e627 Merge pull request #2332 from ttiras/patch-1
Update Docs for useChangeEmail.ts Example
2023-10-24 10:56:55 +01:00
Hassan Ben Jobrane
f011e71ae1 chore: fix typo 2023-10-23 19:49:05 +01:00
Hassan Ben Jobrane
00c363f808 chore: add changeset 2023-10-23 18:10:04 +01:00
Hassan Ben Jobrane
0b2f749ae9 fix: docs: vuejs: update changeEmail docs reference 2023-10-23 18:05:24 +01:00
Hassan Ben Jobrane
cf62a1e6e3 Merge pull request #2331 from nhost/fix/custom-domains/reset-domain
fix(dashboard): allow resetting custom domains
2023-10-20 17:58:06 +01:00
Hassan Ben Jobrane
8df84d782f chore: add changeset 2023-10-20 16:01:47 +01:00
Hassan Ben Jobrane
f0deffafe1 fix(dashboard): allow resetting custom domains 2023-10-20 15:59:54 +01:00
Hassan Ben Jobrane
a291da661d Merge pull request #2321 from MainaMary/bug/update-use-change-password-interface
fix: update useChangePassword hook interface
2023-10-20 11:42:41 +01:00
Mary
66c3193bc9 chore: add changeset 2023-10-20 13:03:00 +03:00
Hassan Ben Jobrane
ac7be49cef Merge pull request #2327 from nhost/chore/run/tweaks
chore(dashboard): fixes and tweaks to services form and dialog
2023-10-19 11:53:32 +01:00
Hassan Ben Jobrane
fa79b77093 chore: add changeset 2023-10-19 11:22:18 +01:00
Hassan Ben Jobrane
5823947933 chore: add missing key to service details dialog 2023-10-19 11:21:11 +01:00
Hassan Ben Jobrane
333837fb57 chore: fix update button icon on service form 2023-10-19 11:10:01 +01:00
Hassan Ben Jobrane
7fae68f6cf Merge pull request #2324 from nhost/changeset-release/main
chore: update versions
2023-10-18 17:09:56 +01:00
github-actions[bot]
f2751f4bac chore: update versions 2023-10-18 16:00:42 +00:00
Hassan Ben Jobrane
089acbbe70 Merge pull request #2320 from nhost/feat/custom-domains
feat(dashboard): custom domains
2023-10-18 16:58:03 +01:00
Nuno Pato
6e08a82f49 Merge pull request #2325 from nhost/docs/custom-domains
chore: docs: add custom domains
2023-10-18 15:49:47 +00:00
Hassan Ben Jobrane
6899ef3b39 chore: run pnpm codegen 2023-10-18 16:20:17 +01:00
Hassan Ben Jobrane
cad3686364 fix: tweak text when dark mode is on 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
8f2c002715 fix: fix custom domains page on small screens 2023-10-18 16:18:45 +01:00
Nuno Pato
b70d61198f fix link to docs 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
d29af2ce6f fix: make sure settings container title supports both a ReactNode and a string 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
cdc992b888 chore: update custom domains header message
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
205a20de87 fix: use correct database host 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
b092b8fe08 chore: tweak database domain description and docs link 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
2d40cbf624 fix: tweak verification box 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
7b591e8c4c fix dns verification values 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
72b425a5bc fix: remove duplicated nhost.run suffix 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
971ff92ab4 chore: remove comment 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
b7f801874d chore: add changeset 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
ff69f30e47 chore: move docs link to the top section 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
cc1932492d fix: only show services that have ports 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
f45037e79f fix: always show CNAME verification panel 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
48658e2925 feat: added run services port domains 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
b90bb6b924 feat: add database domain form 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
de61f45bd5 fix: auth and hasura domain forms 2023-10-18 16:18:45 +01:00
Hassan Ben Jobrane
fd11e5ca2c feat: add Hasura Domain 2023-10-18 16:18:44 +01:00
Hassan Ben Jobrane
7839c786ef chore: run pnpm codegen 2023-10-18 16:18:44 +01:00
Hassan Ben Jobrane
a2bcd6a4b6 feat: add auth domain form 2023-10-18 16:18:44 +01:00
Hassan Ben Jobrane
2cd5b26e0e chore: run pnpm codegen 2023-10-18 16:18:44 +01:00
Hassan Ben Jobrane
559611af70 feat: add upgrade banner to access custom domains 2023-10-18 16:16:37 +01:00
Hassan Ben Jobrane
ffb45f5a49 Merge pull request #2326 from nhost/feat/database/storage-capacity
feat(dashboard): add database storage capacity setting
2023-10-18 15:53:18 +01:00
Hassan Ben Jobrane
451e80ac12 chore: add a warning message that db storage can't be downgraded 2023-10-18 15:19:18 +01:00
Hassan Ben Jobrane
c9f8e523f2 chore: fix upgrade message text 2023-10-18 13:43:37 +01:00
Hassan Ben Jobrane
331ba03768 chore: add changeset 2023-10-18 13:29:42 +01:00
Hassan Ben Jobrane
611b26bc7d chore: fix mocks 2023-10-18 13:29:33 +01:00
Nuno Pato
a446c3efca use custom-domain.com 2023-10-18 12:24:11 +00:00
Hassan Ben Jobrane
24424ae4dc feat: add postgres storage capacity setting 2023-10-18 13:19:43 +01:00
Hassan Ben Jobrane
2a5b705c26 chore: run pnpm codegen & remove deprecated insertFeedback 2023-10-18 13:18:56 +01:00
Nuno Pato
7f3a32d386 use tabs 2023-10-18 11:51:14 +00:00
Nuno Pato
11fa442aa8 Merge branch 'main' into docs/custom-domains 2023-10-18 11:45:19 +00:00
Nuno Pato
5764f46d99 add changeset 2023-10-18 11:36:26 +00:00
Nuno Pato
78d501801b docs: custom domains 2023-10-18 11:34:38 +00:00
David Barroso
cc8cc8d45d chore(docs): database: added extension http (#2323) 2023-10-18 11:42:50 +02:00
Mary
61fc83996b fix: update useChangePassword hook interface 2023-10-17 13:29:49 +03:00
ttiras
9ddb37e9bb Update useChangeEmail.ts
the wrong example has been modified from;
 
 await changeEmail({
    email: 'new@example.com'
  })

to;

 await changeEmail('new@example.com')
2023-10-14 13:53:07 +03:00
Hassan Ben Jobrane
262828f9a1 Merge pull request #2318 from nhost/changeset-release/main
chore: update versions
2023-10-12 16:36:06 +01:00
github-actions[bot]
12f9726ad7 chore: update versions 2023-10-12 13:40:33 +00:00
Hassan Ben Jobrane
845937b552 Merge pull request #2317 from nhost/fix/apollo-integration
fix: integrations: apollo: correct accessToken nullability test
2023-10-12 14:37:49 +01:00
Hassan Ben Jobrane
f777a3380a chore: add changeset 2023-10-12 10:26:00 +01:00
Hassan Ben Jobrane
5081372cab fix: integrations: apollo: correct accessToken nullability test 2023-10-12 10:23:09 +01:00
Hassan Ben Jobrane
82212345c8 Merge pull request #2311 from nhost/changeset-release/main
chore: update versions
2023-10-11 13:54:25 +01:00
Hassan Ben Jobrane
32d3f167c5 Merge pull request #2313 from nhost/fix/quickstarts
fix: quickstarts: toml node version and tsconfig moduleResolution
2023-10-11 12:37:38 +01:00
github-actions[bot]
3d5f1ea922 chore: update versions 2023-10-11 10:24:24 +00:00
Hassan Ben Jobrane
97841ee5e8 Merge pull request #2312 from nhost/fix/dashboard/run-tab
fix: dashboard: disable run tab when developing locally
2023-10-11 11:21:38 +01:00
Hassan Ben Jobrane
4f3a615ebe fix: quickstarts: fix backend functions node version 2023-10-11 10:14:09 +01:00
Hassan Ben Jobrane
8e8197691c fix: quickstarts: change module resolution to node 2023-10-11 10:13:40 +01:00
Hassan Ben Jobrane
e10389ecf6 chore: add changeset 2023-10-11 10:08:11 +01:00
Hassan Ben Jobrane
cbdf6affec fix(dashboard): disable run services tab in local dev mode 2023-10-11 10:07:02 +01:00
Hassan Ben Jobrane
d19406e694 Merge pull request #2309 from nhost/fix/integration/apollo
fix: integrations: apollo: set accessToken to null after TOKEN_CHANGED event on sign-out
2023-10-10 16:24:39 +01:00
Hassan Ben Jobrane
cffc5dc65b Merge pull request #2310 from nhost/chore/ci/stop-dashboard-releases
chore: ci: stop @nhost/dashboard github releases
2023-10-10 16:24:01 +01:00
Hassan Ben Jobrane
2b5cb58553 chore: ci: stop @nhost/dashboard github releases 2023-10-10 16:03:37 +01:00
Hassan Ben Jobrane
7459a9413e chore: add changeset 2023-10-10 15:43:28 +01:00
Hassan Ben Jobrane
56871cc9f7 fix: integrations: apollo: set accessToken to null after TOKEN_CHANGED event on signout 2023-10-10 13:38:04 +01:00
Hassan Ben Jobrane
8f4d66e52d Merge pull request #2308 from nhost/changeset-release/main
chore: update versions
2023-10-10 13:31:53 +01:00
github-actions[bot]
315a820073 chore: update versions 2023-10-10 12:04:28 +00:00
Hassan Ben Jobrane
ca57ad2cbd Merge pull request #2301 from nhost/feat/quickstarts/sveltekit
feat(quickstarts): sveltekit <--> nhost
2023-10-10 13:00:14 +01:00
Hassan Ben Jobrane
40259344eb Merge pull request #2306 from nhost/chore/providers-updated-notice
chore(dashboard): show oauth providers update notice
2023-10-10 12:45:12 +01:00
Hassan Ben Jobrane
4749f60a08 chore: add changeset 2023-10-09 20:12:04 +01:00
Hassan Ben Jobrane
ac1888514d chore: update readme for sveltekit quickstart 2023-10-09 19:52:44 +01:00
Hassan Ben Jobrane
49b4af439b feat: signup/signin via webauthn 2023-10-09 19:23:50 +01:00
Hassan Ben Jobrane
61e03d6c70 chore: make sure deprication notice is under a project 2023-10-09 16:48:01 +01:00
Hassan Ben Jobrane
bec0fce497 chore: add deprication banner 2023-10-09 15:00:58 +01:00
Hassan Ben Jobrane
c01568a7dd chore: add changeset 2023-10-09 14:28:40 +01:00
Hassan Ben Jobrane
e934216a82 chore: bring back providers update alert 2023-10-09 14:26:41 +01:00
Hassan Ben Jobrane
701d6b8c84 feat: sveltekit: add delete pat 2023-10-07 19:47:05 +01:00
Hassan Ben Jobrane
e158e2440a wip: sveltekit: add echo and pat pages 2023-10-07 19:22:29 +01:00
Hassan Ben Jobrane
fbaa657001 wip: sveltekit: refresh access token + create/delete todos 2023-10-05 17:08:55 +01:00
Hassan Ben Jobrane
559db6d0ec wip: sveltekit: auth + todos(create) 2023-10-05 12:48:59 +01:00
Hassan Ben Jobrane
4c844930f1 wip: update: sveltekit 2023-10-04 16:58:59 +01:00
Hassan Ben Jobrane
3ef503ff81 Merge pull request #2298 from nhost/changeset-release/main
chore: update versions
2023-10-04 16:47:51 +02:00
github-actions[bot]
bfcfd236ea chore: update versions 2023-10-04 14:13:38 +00:00
Hassan Ben Jobrane
bfa7033506 Merge pull request #2296 from nhost/feat/query-announcements
feat: dashboard: query announcements
2023-10-04 16:09:48 +02:00
Hassan Ben Jobrane
78c29fcf0e feat: filter expired announcements 2023-10-04 14:40:22 +01:00
Hassan Ben Jobrane
f1b934ed22 chore: remove old announcement provider 2023-10-04 10:52:20 +01:00
Hassan Ben Jobrane
914369c53f feat: add announcements list component 2023-10-03 20:18:41 +01:00
Hassan Ben Jobrane
af379b967e chore: clean up commented code 2023-10-03 17:43:24 +01:00
Hassan Ben Jobrane
c3efb7ec84 chore: add changeset 2023-10-03 17:41:35 +01:00
Hassan Ben Jobrane
27cbd48c8c feat(dashboard): query latest announcement from platform 2023-10-03 17:40:50 +01:00
Hassan Ben Jobrane
236996a903 Merge pull request #2293 from nhost/changeset-release/main
chore: update versions
2023-10-02 13:01:48 +02:00
github-actions[bot]
5d0936bb93 chore: update versions 2023-10-02 10:38:35 +00:00
Hassan Ben Jobrane
733c212f2d Merge pull request #2291 from nhost/chore/announcement/node18
chore: node18 announcement
2023-10-02 12:35:53 +02:00
Hassan Ben Jobrane
8b47549189 Merge pull request #2286 from nhost/chore/ci/disable-github-releases
chore(ci): set createGithubReleases to false
2023-10-02 12:26:10 +02:00
Hassan Ben Jobrane
3c9c1025ce Merge pull request #2287 from nhost/fix/vue-sdk/nested-unref
fix(vue-sdk): correctly unref arrays
2023-10-02 12:25:14 +02:00
Hassan Ben Jobrane
3e46d3873c chore: add changeset 2023-10-02 10:50:46 +01:00
Hassan Ben Jobrane
4cf8820d72 chore: open announcement link in a new tab 2023-10-02 10:39:15 +01:00
Hassan Ben Jobrane
02a11184fb chore: change announcement link 2023-10-02 10:38:04 +01:00
Hassan Ben Jobrane
7214d47cc7 chore: add changeset 2023-09-30 17:54:42 +01:00
Hassan Ben Jobrane
238b77baad fix(vue): correctly unref arrays 2023-09-30 17:53:05 +01:00
Hassan Ben Jobrane
81b8e538b4 chore(ci): set createGithubReleases to false 2023-09-29 17:12:21 +01:00
Hassan Ben Jobrane
563a37e58d Merge pull request #2285 from nhost/changeset-release/main
chore: update versions
2023-09-29 18:05:40 +02:00
github-actions[bot]
bff23720ee chore: update versions 2023-09-29 15:43:43 +00:00
Hassan Ben Jobrane
02cbaeffd2 Merge pull request #2225 from nhost/feat/examples/nextjs-server-components
feat: quickstarts: draft for using server components
2023-09-29 17:40:52 +02:00
Hassan Ben Jobrane
9eb814c79a chore: update readme 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
ebc5913bb3 chore: naming consistency 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
4fe4a16964 chore: add changeset 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
92c475b7a7 chore: add missing refreshToken 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
679b34b031 chore: cleanup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
d3186aefbd refactor: extract session middleware into helper function 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
fdecac9d69 refactor: add high order component for protected pages 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
5077283028 chore: merge oauth handling in middleware 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
f5f662aad1 chore: refactor server actions 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
735b779af7 chore: clean up database setup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
4418d6abcf chore: cleanup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
049e315c30 fix: set correct path on cookie on oauth signin 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
764597538b fix: make sure that hasura-storage-js works on EdgeRuntime 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
c8aea785cc fix: tweak todo item layout 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
e0e44b2ff4 fix: set same path for session cookie 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
12280f7c87 feat: pat list pagination 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
732a4f40ca wip 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
d67fd599e4 feat: todos CRUD 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
a41231927a feat: add signin with pat 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
42ec665950 fix: return refreshToken in getAuthenticationResult 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
7225712a30 chore: update hasura auth 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
6593fdd9bb fix: make sure refreshToken is returned after signin/signup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
40039fece5 Revert "refactor: make sure to return refresh token"
This reverts commit b31b358ca1898bb4173954b8b33059d92cc8c126.
2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
e5fcfb3cd5 refactor: make sure to return refresh token 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
218ec314fb feat(quickstarts): refactor and organize signup/signin 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
9367e91d45 feat: examples: add other sign in methods
Add sign in with google and webauthn
2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
06c640be2c chore: delete unnecessary files 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
ae45be9816 feat(quickstarts): draft for using server components 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
ec4be590d8 Merge pull request #2284 from nhost/chore/ci/fix-release
chore: ci: fix release worflow
2023-09-29 16:56:12 +02:00
Hassan Ben Jobrane
5c51653aa0 chore: fix release worflow 2023-09-29 13:29:44 +01:00
330 changed files with 8082 additions and 2839 deletions

View File

@@ -5,6 +5,5 @@
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@nhost-examples/sveltekit"]
"updateInternalDependencies": "patch"
}

View File

@@ -42,7 +42,7 @@ jobs:
commit: 'chore: update versions'
title: 'chore: update versions'
publish: pnpm run release
createGithubReleases: true
createGithubReleases: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -66,6 +66,8 @@ jobs:
publish-vercel:
name: Publish to Vercel
runs-on: ubuntu-latest
needs:
- test
steps:
- name: Checkout repository
uses: actions/checkout@v3
@@ -93,6 +95,7 @@ jobs:
needs:
- test
- version
- publish-vercel
steps:
- name: Checkout repository
uses: actions/checkout@v3
@@ -138,16 +141,6 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
- name: Create GitHub Release
uses: taiki-e/create-gh-release-action@v1
with:
changelog: dashboard/CHANGELOG.md
token: ${{ secrets.GITHUB_TOKEN }}
prefix: ${{ env.DASHBOARD_PACKAGE }}@
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
- name: Remove tag on failure
if: failure()
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
bump-cli:
name: Bump Dashboard version in the Nhost CLI

View File

@@ -2,5 +2,6 @@
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"eslint.workingDirectories": ["./dashboard"]
"eslint.workingDirectories": ["./dashboard"],
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -1,5 +1,59 @@
# @nhost/dashboard
## 0.20.27
### Patch Changes
- fa79b7709: chore(dashboard): tweaks and fixes to the service form and dialog
- 8df84d782: fix(dashboard): allow resetting custom domains
- @nhost/react-apollo@6.0.0
- @nhost/nextjs@1.13.39
## 0.20.26
### Patch Changes
- 331ba0376: feat(dashboard): add postgres storage capacity modifier in the settings
- b7f801874: feat(dashboard): add new settings page for custom domains
## 0.20.25
### Patch Changes
- @nhost/react-apollo@5.0.38
## 0.20.24
### Patch Changes
- e10389ecf: fix(dashboard): disable run tab when developing locally
- @nhost/react-apollo@5.0.37
## 0.20.23
### Patch Changes
- c01568a7d: chore(dashboard): show alert to update oauth providers
## 0.20.22
### Patch Changes
- c3efb7ec8: feat(dashboard): query latest announcement from platform
## 0.20.21
### Patch Changes
- 3e46d3873: chore: update link to node18 announcement
## 0.20.20
### Patch Changes
- @nhost/react-apollo@5.0.36
- @nhost/nextjs@1.13.38
## 0.20.19
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.20.19",
"version": "0.20.27",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,62 +0,0 @@
import { Button } from '@/components/ui/v2/Button';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { Text } from '@/components/ui/v2/Text';
import { forwardRef, type ForwardedRef } from 'react';
import { twMerge } from 'tailwind-merge';
import AnnouncementContainer, {
type AnnouncementContainerProps,
} from './AnnouncementContainer';
export interface AnnouncementProps extends AnnouncementContainerProps {
/**
* Function called when the announcement is closed.
*/
onClose?: VoidFunction;
/**
* The href to use for the announcement link.
*/
href: string;
}
function Announcement(
{ children, slotProps, onClose, href, ...props }: AnnouncementProps,
ref: ForwardedRef<HTMLDivElement>,
) {
return (
<AnnouncementContainer
{...props}
ref={ref}
className="grid grid-flow-col justify-between gap-4"
slotProps={{
root: {
...(slotProps?.root || {}),
className: twMerge('w-full py-1.5', slotProps?.root?.className),
},
}}
>
<span />
<div className="flex items-center self-center truncate">
<a href={href}>
<Text className="cursor-pointer truncate hover:underline">
{children}
</Text>
</a>
<ArrowRightIcon className="ml-1 h-4 w-4 text-white" />
</div>
<Button
variant="borderless"
onClick={onClose}
aria-label="Close announcement"
size="small"
className="rounded-sm p-1"
>
<XIcon className="opacity-65 h-4 w-4" />
</Button>
</AnnouncementContainer>
);
}
export default forwardRef(Announcement);

View File

@@ -1,66 +0,0 @@
import {
createElement,
forwardRef,
type DetailedHTMLProps,
type ElementType,
type ForwardedRef,
type HTMLProps,
type PropsWithoutRef,
} from 'react';
import { twMerge } from 'tailwind-merge';
export interface AnnouncementContainerProps
extends PropsWithoutRef<
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>
> {
/**
* Custom component to render as.
*/
component?: ElementType<any>;
/**
* Props passed to component slots.
*/
slotProps?: {
/**
* Props passed to the root component.
*/
root?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
/**
* Props passed to the content component.
*/
content?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
};
}
function AnnouncementContainer(
{
component = 'div',
className,
children,
slotProps,
...props
}: AnnouncementContainerProps,
ref: ForwardedRef<HTMLDivElement>,
) {
return createElement(
component,
{
...props,
...(slotProps?.root || {}),
ref,
className: twMerge('w-full overflow-hidden', slotProps?.root?.className),
},
<div
{...(slotProps?.content || {})}
className={twMerge(
'mx-auto max-w-7xl px-5',
className,
slotProps?.content?.className,
)}
>
{children}
</div>,
);
}
export default forwardRef(AnnouncementContainer);

View File

@@ -1,92 +0,0 @@
import { Divider } from '@/components/ui/v2/Divider';
import {
createContext,
useEffect,
useMemo,
useState,
type PropsWithChildren,
type ReactNode,
} from 'react';
import { useInView } from 'react-intersection-observer';
import Announcement from './Announcement';
interface AnnouncementType {
id: string;
content: ReactNode;
href: string;
}
export interface AnnouncementContextProps {
/**
* The announcement to show.
*/
announcement?: AnnouncementType;
/**
* Whether or not to show the announcement.
*/
showAnnouncement?: boolean;
/**
* Function to close the announcement.
*/
handleClose?: () => void;
/**
* Whether or not the announcement is in view.
*/
inView?: boolean;
}
// Note: You can define the active announcement here.
const announcement: AnnouncementType = {
id: 'node-18',
href: 'https://github.com/nhost/nhost/discussions/2239',
content:
"Starting October 1st, we're upgrading to Node.js 18 for improved performance, security, and stability. Learn more.",
};
export const AnnouncementContext = createContext<AnnouncementContextProps>({});
export default function AnnouncementProvider({ children }: PropsWithChildren) {
const { ref, inView } = useInView();
const [showAnnouncement, setShowAnnouncement] = useState(false);
useEffect(() => {
if (
typeof window === 'undefined' ||
!announcement ||
window.localStorage.getItem(announcement.id) === '1'
) {
return;
}
setShowAnnouncement(true);
}, []);
function handleClose() {
setShowAnnouncement(false);
window.localStorage.setItem(announcement?.id, '1');
}
const announcementValue = useMemo(
() => ({ showAnnouncement, announcement, handleClose, inView }),
[inView, showAnnouncement],
);
return (
<AnnouncementContext.Provider value={announcementValue}>
{announcement && showAnnouncement && (
<>
<Announcement
ref={ref}
href={announcement.href}
onClose={handleClose}
>
{announcement.content}
</Announcement>
<Divider />
</>
)}
{children}
</AnnouncementContext.Provider>
);
}

View File

@@ -1,3 +0,0 @@
export * from './Announcement';
export * from './AnnouncementProvider';
export { default as useAnnouncement } from './useAnnouncement';

View File

@@ -1,14 +0,0 @@
import { useContext } from 'react';
import { AnnouncementContext } from './AnnouncementProvider';
export default function useAnnouncement() {
const context = useContext(AnnouncementContext);
if (!context) {
throw new Error(
'useAnnouncement must be used within an AnnouncementProvider',
);
}
return context;
}

View File

@@ -0,0 +1,28 @@
import { Alert } from '@/components/ui/v2/Alert';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
export default function DepricationNotice() {
const { currentProject } = useCurrentWorkspaceAndProject();
return (
!currentProject?.providersUpdated && (
<Alert severity="warning" className="grid place-content-center">
<Text color="warning" className="max-w-3xl text-sm">
On December 1st the old backend domain will cease to work. You need to
make sure your client is instantiated using the subdomain and region
and update your oauth2 settings. You can find more information{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://github.com/nhost/nhost/discussions/2303"
>
here
</a>
.
</Text>
</Alert>
)
);
}

View File

@@ -0,0 +1 @@
export { default as ContactUs } from './DepricationNotice';

View File

@@ -0,0 +1,100 @@
import { useDialog } from '@/components/common/DialogProvider';
import { NhostIcon } from '@/components/presentational/NhostIcon';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { ChangePlanModal } from '@/features/projects/common/components/ChangePlanModal';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import Image from 'next/image';
interface UpgradeToProBannerProps {
title: string;
description: string;
}
export default function UpgradeToProBanner({
title,
description,
}: UpgradeToProBannerProps) {
const { openDialog, openAlertDialog } = useDialog();
const isOwner = useIsCurrentUserOwner();
return (
<Box
sx={{ backgroundColor: 'primary.light' }}
className="flex flex-col p-4 space-y-4 rounded-md lg:flex-row lg:items-center lg:space-y-0"
>
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2">
<div className="flex flex-col space-y-2 xs:flex-row xs:space-y-0 xs:space-x-2">
<Text>Available with</Text>
<div className="flex flex-row space-x-2">
<NhostIcon />
<Text sx={{ color: 'primary.main' }} className="font-semibold">
Nhost Pro
</Text>
</div>
</div>
<Text variant="h3">{title}</Text>
<Text>{description}</Text>
</div>
<div className="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
<Button
className="rounded-md"
onClick={() => {
if (isOwner) {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
},
});
} else {
openAlertDialog({
title: "You can't upgrade this project",
payload: (
<Text variant="subtitle1" component="span">
Ask an owner of this workspace to upgrade the project.
</Text>
),
props: {
secondaryButtonText: 'I understand',
hidePrimaryAction: true,
},
});
}
}}
>
Upgrade to Pro
</Button>
<Link
href="https://nhost.io/pricing"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium text-center"
sx={{
color: 'text.secondary',
}}
>
See all features
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
</Link>
</div>
</div>
<div className="max-w-xs mx-auto">
<Image
src="/illustration-unbox.png"
width={400}
height={260}
objectFit="contain"
/>
</div>
</Box>
);
}

View File

@@ -0,0 +1 @@
export { default as UpgradeToProBanner } from './UpgradeToProBanner';

View File

@@ -128,7 +128,11 @@ export default function SettingsContainer({
icon}
<div className="grid grid-flow-row gap-1">
<Text className="text-lg font-semibold">{title}</Text>
{typeof title === 'string' ? (
<Text className="text-lg font-semibold">{title}</Text>
) : (
title
)}
{description && <Text color="secondary">{description}</Text>}
</div>

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
@@ -45,44 +46,47 @@ export default function SettingsLayout({
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex flex-col flex-auto w-full overflow-scroll overflow-x-hidden"
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
>
<RetryableErrorBoundary>
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row gap-2 place-content-center"
>
<Text color="warning" className="text-sm ">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code
className={twMerge(
'rounded-md px-2 py-px',
theme.palette.mode === 'dark'
? 'bg-brown text-copper'
: 'bg-slate-200 text-slate-700',
)}
>
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository and
you only want these changes to apply to a subset of them, please
check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/cli/overlays"
>
docs.nhost.io/cli/overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
<div className="flex flex-col space-y-2">
<DepricationNotice />
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row place-content-center gap-2"
>
<Text color="warning" className="text-sm ">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code
className={twMerge(
'rounded-md px-2 py-px',
theme.palette.mode === 'dark'
? 'bg-brown text-copper'
: 'bg-slate-200 text-slate-700',
)}
>
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository
and you only want these changes to apply to a subset of them,
please check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/cli/overlays"
>
docs.nhost.io/cli/overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
</div>
{children}
</RetryableErrorBoundary>
</Box>

View File

@@ -200,6 +200,14 @@ export default function SettingsSidebar({
>
Secrets
</SettingsNavLink>
<SettingsNavLink
href="/custom-domains"
exact={false}
onClick={handleSelect}
>
Custom Domains
</SettingsNavLink>
</List>
</nav>
</Box>

View File

@@ -0,0 +1,35 @@
import type { ForwardedRef, SVGProps } from 'react';
import { forwardRef } from 'react';
function NhostIcon(
props: SVGProps<SVGSVGElement>,
ref: ForwardedRef<SVGSVGElement>,
) {
return (
<svg
ref={ref}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Logo of Nhost"
{...props}
>
<g clipPath="url(#clip0_9802_20458)">
<rect width="24" height="24" fill="#0052CD" />
<path
d="M17.4656 7.39804L12.4705 4.51369C12.0223 4.25553 11.466 4.25553 11.0169 4.51369C10.5688 4.77276 10.2906 5.25455 10.2906 5.77179V6.14813L9.96517 5.95996C9.51702 5.70179 8.96069 5.70179 8.51163 5.95996C8.06348 6.21903 7.78531 6.70082 7.78531 7.21896V7.5953L7.45988 7.40713C7.01173 7.14897 6.4554 7.14897 6.00634 7.40713C5.55819 7.66621 5.28003 8.14799 5.28003 8.66614V17.7037C5.28003 17.9637 5.43093 18.2055 5.66546 18.3182C5.89908 18.4318 6.1827 18.4009 6.38632 18.24L8.86342 16.2865L12.6832 18.4918C12.7886 18.5527 12.9068 18.5827 13.025 18.5827C13.1431 18.5827 13.2613 18.5518 13.3668 18.4918C13.5777 18.37 13.7086 18.1437 13.7086 17.9001V12.4613C13.7086 11.5687 13.2286 10.7378 12.4559 10.2915L11.2033 9.56789V5.7727C11.2033 5.57998 11.3069 5.4 11.4742 5.30364C11.6414 5.20728 11.8487 5.20728 12.0159 5.30364L17.0111 8.18708C17.5028 8.4707 17.8083 9.00066 17.8083 9.56789V16.3402C17.8083 16.5329 17.7046 16.7129 17.5374 16.8092L16.2138 17.5737V11.0142C16.2138 10.1215 15.7339 9.29064 14.9612 8.84431L11.8859 7.06897V8.12072L14.5058 9.63334C14.9976 9.91696 15.303 10.446 15.303 11.0142V17.9673C15.303 18.21 15.4339 18.4373 15.6448 18.5591C15.7502 18.62 15.8684 18.65 15.9866 18.65C16.1048 18.65 16.2229 18.6191 16.3284 18.5591L17.9937 17.5974C18.4419 17.3383 18.72 16.8565 18.72 16.3383V9.56608C18.7182 8.67614 18.2382 7.84438 17.4656 7.39804ZM11.9987 11.0805C12.4905 11.3641 12.7959 11.8932 12.7959 12.4613V17.5064L9.63246 15.6802L10.6478 14.8803C10.9996 14.603 11.2014 14.1876 11.2014 13.7394V10.6215L11.9987 11.0805ZM10.2906 10.0942V13.7376C10.2906 13.9049 10.2152 14.0603 10.0842 14.163L6.19088 17.2328V8.66523C6.19088 8.47251 6.29451 8.29253 6.46177 8.19617C6.62903 8.09981 6.83629 8.09981 7.00355 8.19617L7.78531 8.64705V15.1057L8.69616 14.3876V7.21896C8.69616 7.02625 8.79979 6.84626 8.96705 6.7499C9.13431 6.65355 9.34157 6.65355 9.50883 6.7499L10.2906 7.20078V9.04157L9.37975 8.51524V9.56789L10.2906 10.0942Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_9802_20458">
<rect width="24" height="24" rx="4" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export default forwardRef(NhostIcon);

View File

@@ -0,0 +1 @@
export { default as NhostIcon } from './NhostIcon';

View File

@@ -0,0 +1,100 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v2/Button';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useConfirmProvidersUpdatedMutation } from '@/utils/__generated__/graphql';
import { useTheme } from '@mui/material';
import { useState } from 'react';
import toast from 'react-hot-toast';
export default function ProvidersUpdatedAlert() {
const theme = useTheme();
const { openAlertDialog } = useDialog();
const [confirmed, setConfirmed] = useState(true);
const { currentProject } = useCurrentWorkspaceAndProject();
const [confirmProvidersUpdated] = useConfirmProvidersUpdatedMutation({
variables: { id: currentProject?.id },
});
async function handleSubmitConfirmation() {
const confirmProvidersUpdatedPromise = confirmProvidersUpdated();
await toast.promise(
confirmProvidersUpdatedPromise,
{
loading: 'Confirming...',
success: 'Your settings have been updated successfully.',
error: 'An error occurred while trying to confirm the message.',
},
getToastStyleProps(),
);
setConfirmed(false);
}
function handleOpenConfirmationDialog() {
openAlertDialog({
title: 'Confirm all providers updated?',
payload: (
<Text variant="subtitle1" component="span">
Please make sure to update all providers before continuing. Your
sign-in flows might break if you don&apos;t.
</Text>
),
props: {
onPrimaryAction: handleSubmitConfirmation,
},
});
}
if (!confirmed) {
return null;
}
return (
<Alert
severity="warning"
className="grid items-center grid-flow-row gap-2 p-4 place-items-center lg:grid-flow-col lg:place-content-between"
>
<div className="grid grid-flow-row gap-1 text-left">
<Text className="font-semibold">
Please update the Redirect URL for all providers being used
</Text>
<Text className="text-sm+">
We are deprecating your project&apos;s old DNS name in favor of
individual DNS names for each service. Please make sure to update your
providers to use the new auth specific URL under <b>Redirect URL</b>{' '}
before the 1st of February 2023.{' '}
<Link
href="https://github.com/nhost/nhost/discussions/1319"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
Read the discussion here.
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
</Link>
</Text>
</div>
<Button
variant="borderless"
className={
theme.palette.mode === 'dark'
? 'text-white hover:bg-brown'
: 'text-black hover:bg-orange-300'
}
onClick={handleOpenConfirmationDialog}
>
I have updated all Redirect URLs
</Button>
</Alert>
);
}

View File

@@ -0,0 +1 @@
export { default as ProvidersUpdatedAlert } from './ProvidersUpdatedAlert';

View File

@@ -0,0 +1,44 @@
import type { IconProps } from '@/components/ui/v2/icons';
function ArrowsClockwise(props: IconProps) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-label="Update"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11.0103 6.23227H14.0103V3.23227"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M4.11084 4.11091C4.62156 3.60019 5.22788 3.19506 5.89517 2.91866C6.56246 2.64226 7.27766 2.5 7.99993 2.5C8.7222 2.5 9.4374 2.64226 10.1047 2.91866C10.772 3.19506 11.3783 3.60019 11.889 4.11091L14.0103 6.23223"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M4.98975 9.76773H1.98975V12.7677"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M11.8892 11.8891C11.3785 12.3998 10.7722 12.8049 10.1049 13.0813C9.43762 13.3577 8.72242 13.5 8.00015 13.5C7.27788 13.5 6.56269 13.3577 5.89539 13.0813C5.2281 12.8049 4.62179 12.3998 4.11107 11.8891L1.98975 9.76776"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
}
ArrowsClockwise.displayName = 'NhostArrowsClockwise';
export default ArrowsClockwise;

View File

@@ -0,0 +1 @@
export { default as ArrowsClockwise } from './ArrowsClockwise';

View File

@@ -24,6 +24,13 @@ query GetAuthenticationSettings($appId: uuid!) {
expiresIn
}
}
resources {
networking {
ingresses {
fqdn
}
}
}
user {
email {
allowed

View File

@@ -0,0 +1,161 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useGetPostgresSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
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({
capacity: Yup.number().required(),
});
export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthDomain() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const {
data,
loading,
error,
refetch: refetchPostgresSettings,
} = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const capacity =
data?.config?.postgres?.resources?.storage?.capacity ??
currentProject.plan.featureMaxDbSize;
const [updateConfig] = useUpdateConfigMutation();
const form = useForm<{ capacity: number }>({
reValidateMode: 'onSubmit',
defaultValues: { capacity },
resolver: yupResolver(validationSchema),
});
const { formState, register, reset } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (data && !loading) {
reset({ capacity });
}
}, [loading, data, reset, capacity]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Auth Domain Settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(formValues: AuthDomainFormValues) {
try {
await toast.promise(
updateConfig({
variables: {
appId: currentProject.id,
config: {
postgres: {
resources: {
storage: {
capacity: formValues.capacity,
},
},
},
},
},
}),
{
loading: `Database storage capacity is being updated...`,
success: `Database storage capacity has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the database storage capacity.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchPostgresSettings();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Storage capacity"
description="Specify the storage capacity for your PostgreSQL database."
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col"
>
{currentProject.plan.isFree && (
<UpgradeNotification message="Unlock by upgrading your project to the Pro plan." />
)}
<Box className="grid grid-flow-row lg:grid-cols-5">
<Input
{...register('capacity')}
id="capacity"
name="capacity"
type="number"
fullWidth
disabled={currentProject.plan.isFree}
className="lg:col-span-2"
error={Boolean(formState.errors.capacity?.message)}
helperText={formState.errors.capacity?.message}
slotProps={{
inputRoot: {
min: capacity,
},
}}
/>
</Box>
{!currentProject.plan.isFree && (
<Box
className="grid items-center grid-flow-col gap-1 p-3 rounded-lg shadow-sm place-content-between"
sx={{ backgroundColor: 'grey.200' }}
>
<Text>
Please note that once you increase the storage, it cannot be
reduced.
</Text>
</Box>
)}
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as DatabaseStorageCapacity } from './DatabaseStorageCapacity';

View File

@@ -9,6 +9,11 @@ query GetPostgresSettings($appId: uuid!) {
__typename
postgres {
version
resources {
storage {
capacity
}
}
}
}
}

View File

@@ -18,6 +18,13 @@ query GetHasuraSettings($appId: uuid!) {
events {
httpPoolSize
}
resources {
networking {
ingresses {
fqdn
}
}
}
}
}
}

View File

@@ -0,0 +1,47 @@
import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { useGetAnnouncementsQuery } from '@/utils/__generated__/graphql';
import formatDistance from 'date-fns/formatDistance';
export default function Announcements() {
const { data, loading, error } = useGetAnnouncementsQuery();
const announcements = data?.announcements || [];
if (loading || error) {
return null;
}
return (
<section>
<Text color="secondary" className="mb-2">
Latest announcements
</Text>
<List className="relative space-y-4 border-l border-gray-200 dark:border-gray-700">
{announcements.map((item) => (
<ListItem.Root key={item.id} className="ml-4">
<div className="flex flex-col">
<time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
{formatDistance(new Date(item.createdAt), new Date(), {
addSuffix: true,
})}
</time>
<a href={item.href} target="_blank" rel="noopener noreferrer">
<ListItem.Button
dense
aria-label={`View ${item.content}`}
className="!p-1"
>
<p className="text-sm">{item.content}</p>
</ListItem.Button>
</a>
</div>
<div className="absolute top-[0.15rem] -ml-[1.4rem] h-3 w-3 rounded-full border border-white bg-gray-200 dark:border-gray-900 dark:bg-gray-700" />
</ListItem.Root>
))}
</List>
</section>
);
}

View File

@@ -0,0 +1 @@
export { default as Announcements } from './Announcements';

View File

@@ -8,6 +8,7 @@ import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { Announcements } from '@/features/projects/common/components/Announcements';
import { EditWorkspaceNameForm } from '@/features/projects/workspaces/components/EditWorkspaceNameForm';
import type { Workspace } from '@/types/application';
import Image from 'next/image';
@@ -38,6 +39,8 @@ export default function WorkspaceSidebar({
)}
{...props}
>
<Announcements />
<section className="grid grid-flow-row gap-2">
<Text color="secondary">My Workspaces</Text>

View File

@@ -142,6 +142,7 @@ export default function useProjectRoutes() {
exact: false,
label: 'Run',
icon: <ServicesIcon />,
disabled: !isPlatform,
},
...nhostRoutes,
];

View File

@@ -0,0 +1,153 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import {
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
type ConfigIngressUpdateInput,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
auth_fqdn: Yup.string(),
});
export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthDomain() {
const { maintenanceActive } = useUI();
const [isVerified, setIsVerified] = useState(false);
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const form = useForm<{ auth_fqdn: string }>({
reValidateMode: 'onSubmit',
defaultValues: { auth_fqdn: null },
resolver: yupResolver(validationSchema),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: {
appId: currentProject.id,
},
});
const { networking } = data?.config?.auth?.resources || {};
const initialValue = networking?.ingresses?.[0]?.fqdn?.[0];
useEffect(() => {
if (!loading && data) {
form.reset({ auth_fqdn: initialValue });
}
}, [data, loading, form, initialValue]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Auth Domain Settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, register, watch } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const auth_fqdn = watch('auth_fqdn');
async function handleSubmit(formValues: AuthDomainFormValues) {
const ingresses: ConfigIngressUpdateInput[] =
formValues.auth_fqdn.length > 0 ? [{ fqdn: [formValues.auth_fqdn] }] : [];
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
resources: {
networking: {
ingresses,
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Auth domain is being updated...`,
success: `Auth domain has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the auth domain.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Auth Domain"
description="Enter below your custom domain for the authentication service."
slotProps={{
submitButton: {
disabled:
!isDirty || maintenanceActive || (!isVerified && !initialValue),
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row px-4 gap-y-4 gap-x-4 lg:grid-cols-5"
>
<Input
{...register('auth_fqdn')}
id="auth_fqdn"
name="auth_fqdn"
type="string"
fullWidth
className="col-span-5 lg:col-span-2"
placeholder="auth.mydomain.dev"
error={Boolean(formState.errors.auth_fqdn?.message)}
helperText={formState.errors.auth_fqdn?.message}
slotProps={{ inputRoot: { min: 1, max: 100 } }}
/>
<div className="col-span-5 row-start-2">
<VerifyDomain
recordType="CNAME"
hostname={auth_fqdn}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as AuthDomain } from './AuthDomain';

View File

@@ -0,0 +1,61 @@
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import { useState } from 'react';
import * as Yup from 'yup';
const validationSchema = Yup.object({
database_fqdn: Yup.string().required(),
});
export type DatabaseDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function DatabaseDomain() {
const { currentProject } = useCurrentWorkspaceAndProject();
const [dbFQDN, setDbFQDN] = useState('');
const postgresHost = generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'db',
).replace('https://', '');
return (
<SettingsContainer
title="Database Domain"
description="Enter below your custom domain for the PostgreSQL database to verify. Once verified there is no need to save this value as no configuration on our end is required."
slotProps={{
submitButton: {
hidden: true,
},
footer: {
className: 'hidden',
},
}}
className="grid grid-flow-row px-4 gap-y-4 gap-x-4 lg:grid-cols-5"
>
<Input
id="database_fqdn"
name="database_fqdn"
type="string"
fullWidth
className="col-span-5 lg:col-span-2"
placeholder="db.mydomain.dev"
onChange={(e) => {
setDbFQDN(e.target.value);
}}
slotProps={{ inputRoot: { min: 1, max: 100 } }}
/>
<div className="col-span-5 row-start-2">
<VerifyDomain
recordType="CNAME"
hostname={dbFQDN}
value={`${postgresHost}.`}
/>
</div>
</SettingsContainer>
);
}

View File

@@ -0,0 +1 @@
export { default as DatabaseDomain } from './DatabaseDomain';

View File

@@ -0,0 +1,155 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import {
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
type ConfigIngressUpdateInput,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
hasura_fqdn: Yup.string(),
});
export type HasuraDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraDomain() {
const { maintenanceActive } = useUI();
const [isVerified, setIsVerified] = useState(false);
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const form = useForm<{ hasura_fqdn: string }>({
reValidateMode: 'onSubmit',
defaultValues: { hasura_fqdn: null },
resolver: yupResolver(validationSchema),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: {
appId: currentProject.id,
},
});
const { networking } = data?.config?.hasura?.resources || {};
const initialValue = networking?.ingresses?.[0]?.fqdn?.[0];
useEffect(() => {
if (!loading && data) {
form.reset({ hasura_fqdn: initialValue });
}
}, [data, loading, form, initialValue]);
if (loading) {
return (
<ActivityIndicator
delay={0}
label="Loading Hasura Domain..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, register, watch } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const hasura_fqdn = watch('hasura_fqdn');
async function handleSubmit(formValues: HasuraDomainFormValues) {
const ingresses: ConfigIngressUpdateInput[] =
formValues.hasura_fqdn.length > 0
? [{ fqdn: [formValues.hasura_fqdn] }]
: [];
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
resources: {
networking: {
ingresses,
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Hasura domain is being updated...`,
success: `Hasura domain has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the Hasura domain.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Hasura Domain"
description="Enter below your custom domain for the Hasura/GraphQL service."
slotProps={{
submitButton: {
disabled:
!isDirty || maintenanceActive || (!isVerified && !initialValue),
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row px-4 gap-y-4 gap-x-4 lg:grid-cols-5"
>
<Input
{...register('hasura_fqdn')}
id="hasura_fqdn"
name="hasura_fqdn"
type="string"
fullWidth
className="col-span-5 lg:col-span-2"
placeholder="auth.mydomain.dev"
error={Boolean(formState.errors.hasura_fqdn?.message)}
helperText={formState.errors.hasura_fqdn?.message}
slotProps={{ inputRoot: { min: 1, max: 100 } }}
/>
<div className="col-span-5 row-start-2">
<VerifyDomain
recordType="CNAME"
hostname={hasura_fqdn}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as HasuraDomain } from './HasuraDomain';

View File

@@ -0,0 +1,88 @@
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { RunServicePortDomain } from '@/features/projects/custom-domains/settings/components/RunServicePortDomain';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useMemo } from 'react';
export default function RunServiceDomains() {
const { currentProject, currentWorkspace } = useCurrentWorkspaceAndProject();
const {
data,
loading,
// refetch: refetchServices, // TODO refetch after update
} = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: 1000, // TODO consider pagination
offset: 0,
},
});
const services = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Run Services Domains..."
className="justify-center"
/>
);
}
return (
<div className="flex flex-col gap-6">
{services
.filter((service) => service.config?.ports?.length > 0)
.map((service) => (
<SettingsContainer
key={service.id}
title={
<div className="flex flex-row items-center">
<Text className="text-lg font-semibold">
{service.config?.name ?? 'unset'}
</Text>
<Link
href={`/${currentWorkspace.slug}/${currentProject.slug}/services`}
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<ArrowSquareOutIcon className="mb-1 ml-1 h-4 w-4" />
</Link>
</div>
}
description="Enter below your custom domain for the published ports."
docsTitle={service.config?.name ?? 'unset'}
slotProps={{
submitButton: {
hidden: true,
},
footer: {
className: 'hidden',
},
}}
className="grid gap-y-4 gap-x-4 px-4"
>
{service.config?.ports?.map((port) => (
<RunServicePortDomain
key={String(port.port)}
service={service}
port={port.port}
/>
))}
</SettingsContainer>
))}
</div>
);
}

View File

@@ -0,0 +1 @@
export { default as RunServiceDomains } from './RunServiceDomains';

View File

@@ -0,0 +1,159 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { Button } from '@/components/ui/v2/Button';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import { useUpdateRunServiceConfigMutation } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { type RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
interface RunServicePortProps {
service: RunService;
port: number;
}
const validationSchema = Yup.object({
runServicePortFQDN: Yup.string(),
});
export type RunServicePortFormValues = Yup.InferType<typeof validationSchema>;
export default function RunServicePortDomain({
service,
port,
}: RunServicePortProps) {
const { maintenanceActive } = useUI();
const [loading, setLoading] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateRunServiceConfig] = useUpdateRunServiceConfigMutation();
const runServicePort = service.config.ports.find((p) => p.port === port);
const initialValue = runServicePort?.ingresses?.[0]?.fqdn?.[0];
const form = useForm<{ runServicePortFQDN: string }>({
reValidateMode: 'onSubmit',
defaultValues: {
runServicePortFQDN: initialValue,
},
resolver: yupResolver(validationSchema),
});
const { formState, register, watch } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const runServicePortFQDN = watch('runServicePortFQDN');
async function handleSubmit(formValues: RunServicePortFormValues) {
try {
setLoading(true);
await toast.promise(
updateRunServiceConfig({
variables: {
appID: currentProject.id,
serviceID: service.id,
config: {
ports: service?.config?.ports?.map((p) => {
// exclude the `__typename` because the mutation will fail otherwise
const { __typename, ...rest } = p;
if (rest.port === port) {
return {
...rest,
ingresses:
formValues.runServicePortFQDN.length > 0
? [{ fqdn: [formValues.runServicePortFQDN] }]
: [],
};
}
return {
...rest,
// exclude the `__typename` because the mutation will fail otherwise
ingresses: rest.ingresses.map((item) => ({
fqdn: item.fqdn,
})),
};
}),
},
},
}),
{
loading: `Port ${port} is being updated...`,
success: `Port ${port} has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Port ${port}.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
// TODO refetch the service config
// await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
} finally {
setLoading(false);
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<div className="space-y-2">
<Text className="text-sm font-semibold">{`${runServicePort.type} <--> ${runServicePort.port}`}</Text>
<div className="flex flex-row space-x-4">
<Input
{...register('runServicePortFQDN')}
id="runServicePortFQDN"
name="runServicePortFQDN"
type="string"
fullWidth
className=""
placeholder={`${service.config?.name ?? 'unset'}-${
runServicePort.port
}.mydomain.dev`}
error={Boolean(formState.errors.runServicePortFQDN?.message)}
helperText={formState.errors.runServicePortFQDN?.message}
slotProps={{
inputRoot: { min: 1, max: 100 },
}}
/>
<Button
variant="outlined"
type="submit"
disabled={
loading ||
!isDirty ||
maintenanceActive ||
(!isVerified && !initialValue)
}
>
Save
</Button>
</div>
</div>
<div className="col-span-5 row-start-2 mt-4">
<VerifyDomain
recordType="CNAME"
hostname={runServicePortFQDN}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1 @@
export { default as RunServicePortDomain } from './RunServicePortDomain';

View File

@@ -0,0 +1,155 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Text } from '@/components/ui/v2/Text';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { useDnsLookupCnameLazyQuery } from '@/utils/__generated__/graphql';
import { ApolloError } from '@apollo/client';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
interface VerifyDomainProps {
recordType: string;
hostname: string;
value: string;
onHostNameVerified?: () => void;
}
export default function VerifyDomain({
recordType,
hostname,
value,
onHostNameVerified,
}: VerifyDomainProps) {
const [verificationFailed, setVerificationFailed] = useState(false);
const [verificationSucceeded, setVerificationSucceeded] = useState(false);
const [loading, setLoading] = useState(false);
const [fireLookupCNAME] = useDnsLookupCnameLazyQuery();
const handleVerifyDomain = async () => {
setLoading(true);
try {
await toast.promise(
fireLookupCNAME({
variables: {
hostname,
},
}).then(({ data: { dnsLookupCNAME } }) => {
if (dnsLookupCNAME !== value) {
throw new Error(`Could not verify ${hostname}`);
}
}),
{
loading: `Verifying ${hostname} ...`,
success: () => {
setVerificationFailed(false);
setVerificationSucceeded(true);
setLoading(false);
onHostNameVerified?.();
return `${hostname} has been verified.`;
},
error: (arg: Error | ApolloError) => {
setVerificationFailed(true);
setVerificationSucceeded(false);
setLoading(false);
if (arg instanceof ApolloError) {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } =
(internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
`An error occurred while trying to verify ${hostname}. Please try again.`
);
}
return arg.message;
},
},
getToastStyleProps(),
);
} catch (error) {
// Note: The toast will handle the error.
}
};
return (
<Box
sx={[
{ backgroundColor: 'primary.light' },
verificationFailed && {
backgroundColor: 'error.light',
color: 'error.main',
},
verificationSucceeded && {
backgroundColor: 'success.light',
color: 'success.dark',
},
]}
className="flex flex-col p-4 space-y-4 rounded-md"
>
<div className="flex flex-row items-center justify-between">
{!verificationFailed && !verificationSucceeded && (
<Text>
Add the record below in your DNS provider to verify {hostname}
</Text>
)}
{verificationSucceeded && (
<Text>
<span className="font-semibold">{hostname}</span> was verified
successfully. Hit save to apply.
</Text>
)}
{verificationFailed && (
<Text>
An error occurred while trying to verify{' '}
<span className="font-semibold">{hostname}</span>. Make sure you
correctly added the <span className="font-semibold">CNAME</span> and
try again.
</Text>
)}
</div>
<div className="relative flex flex-col text-slate-500">
<div className="flex space-x-2">
<Text>Record type: </Text>
<Text className="font-bold">{recordType}</Text>
</div>
<div className="flex space-x-2">
<Text>Host:</Text>
<Text className="font-bold">{hostname}</Text>
</div>
<div className="flex flex-row space-x-2">
<Text>Value:</Text>
<Text className="font-bold">{value}</Text>
<IconButton
aria-label="Copy Personal Access Token"
variant="borderless"
color="secondary"
onClick={() => copy(value, 'CNAME Value')}
>
<CopyIcon className="w-4 h-4" />
</IconButton>
</div>
<Button
disabled={loading || !hostname}
onClick={handleVerifyDomain}
className="mt-4 sm:absolute sm:bottom-0 sm:right-0 sm:mt-0"
>
Verify
</Button>
</div>
</Box>
);
}

View File

@@ -0,0 +1 @@
export { default as VerifyDomain } from './VerifyDomain';

View File

@@ -3,6 +3,7 @@ import { Form } from '@/components/form/Form';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
@@ -344,7 +345,7 @@ export default function ServiceForm({
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -384,7 +385,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -415,7 +416,7 @@ export default function ServiceForm({
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -459,7 +460,7 @@ export default function ServiceForm({
{createServiceFormError && (
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
className="grid items-center justify-between grid-flow-col px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}
@@ -482,7 +483,7 @@ export default function ServiceForm({
<Button
type="submit"
disabled={isSubmitting}
startIcon={<PlusIcon />}
startIcon={serviceID ? <ArrowsClockwise /> : <PlusIcon />}
>
{serviceID ? 'Update' : 'Create'}
</Button>

View File

@@ -55,7 +55,8 @@ export default function ServiceDetailsDialog({
.filter((port) => port.publish)
.map((port) => (
<InfoCard
title={`${port.type}:${port.port}`}
key={String(port.port)}
title={`${port.type} <--> ${port.port}`}
value={getPortURL(port.port)}
/>
))}

View File

@@ -0,0 +1,3 @@
query dnsLookupCNAME($hostname: String!) {
dnsLookupCNAME(hostname: $hostname)
}

View File

@@ -1,5 +1,12 @@
mutation UpdateConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
updateConfig(appID: $appId, config: $config) {
id: __typename
postgres {
resources {
storage {
capacity
}
}
}
}
}

View File

@@ -46,6 +46,7 @@ fragment Project on apps {
name
price
isFree
featureMaxDbSize
}
githubRepository {
fullName

View File

@@ -0,0 +1,14 @@
query getAnnouncements($limit: Int) {
announcements(
order_by: { createdAt: desc }
limit: $limit
where: {
_or: [{ expiresAt: { _is_null: true } }, { expiresAt: { _gt: now } }]
}
) {
id
href
content
createdAt
}
}

View File

@@ -36,10 +36,12 @@ query getRunServices(
port
type
publish
ingresses {
fqdn
}
}
}
}
runServices_aggregate {
aggregate {
count

View File

@@ -0,0 +1,5 @@
mutation confirmProvidersUpdated($id: uuid!) {
updateApp(pk_columns: { id: $id }, _set: { providersUpdated: true }) {
id
}
}

View File

@@ -1,5 +0,0 @@
mutation insertFeedbackOne($feedback: feedback_insert_input!) {
insertFeedbackOne(object: $feedback) {
id
}
}

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
@@ -67,5 +68,10 @@ export default function BackupsPage() {
}
BackupsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { InlineCode } from '@/components/presentational/InlineCode';
import { DataBrowserEmptyState } from '@/features/database/dataGrid/components/DataBrowserEmptyState';
import { DataBrowserLayout } from '@/features/database/dataGrid/components/DataBrowserLayout';
@@ -35,5 +36,10 @@ export default function DataBrowserDatabaseDetailsPage() {
DataBrowserDatabaseDetailsPage.getLayout = function getLayout(
page: ReactElement,
) {
return <DataBrowserLayout>{page}</DataBrowserLayout>;
return (
<DataBrowserLayout>
<DepricationNotice />
{page}
</DataBrowserLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import type { DeploymentStatus } from '@/components/presentational/StatusCircle';
@@ -105,7 +106,7 @@ export default function DeploymentDetailsPage() {
<Text color="secondary">{relativeDateOfDeployment}</Text>
</div>
</div>
<div className=" flex items-center">
<div className="flex items-center ">
<Link
className="self-center font-mono font-medium"
target="_blank"
@@ -139,7 +140,7 @@ export default function DeploymentDetailsPage() {
{deployment.deploymentLogs.map((log) => (
<div key={log.id} className="flex font-mono">
<div className=" mr-2 flex-shrink-0">
<div className="mr-2 flex-shrink-0 ">
{format(parseISO(log.createdAt), 'HH:mm:ss')}:
</div>
<div className="break-all">{log.message}</div>
@@ -152,5 +153,10 @@ export default function DeploymentDetailsPage() {
}
DeploymentDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { useUI } from '@/components/common/UIProvider';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
@@ -67,5 +68,10 @@ export default function DeploymentsPage() {
}
DeploymentsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
@@ -351,6 +352,7 @@ GraphQLPage.getLayout = function getLayout(page: ReactElement) {
},
}}
>
<DepricationNotice />
{page}
</ProjectLayout>
);

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
@@ -114,5 +115,10 @@ export default function HasuraPage() {
}
HasuraPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { ApplicationErrored } from '@/features/projects/common/components/ApplicationErrored';
import { ApplicationLive } from '@/features/projects/common/components/ApplicationLive';
@@ -49,5 +50,10 @@ export default function AppIndexPage() {
}
AppIndexPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
@@ -134,5 +135,10 @@ export default function LogsPage() {
}
LogsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
@@ -126,5 +127,10 @@ export default function MetricsPage() {
}
MetricsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -12,6 +12,7 @@ import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/
import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import {
ServiceForm,
@@ -43,7 +44,7 @@ export default function ServicesPage() {
const router = useRouter();
const { openDrawer, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject.plan.isFree;
const isPlanFree = currentProject?.plan?.isFree;
const [currentPage, setCurrentPage] = useState(
parseInt(router.query.page as string, 10) || 1,
@@ -258,5 +259,10 @@ export default function ServicesPage() {
}
ServicesPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
return (
<ProjectLayout>
<DepricationNotice />
{page}
</ProjectLayout>
);
};

View File

@@ -0,0 +1,71 @@
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import { Container } from '@/components/layout/Container';
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { Box } from '@/components/ui/v2/Box';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { AuthDomain } from '@/features/projects/custom-domains/settings/components/AuthDomain';
import { DatabaseDomain } from '@/features/projects/custom-domains/settings/components/DatabaseDomain';
import { HasuraDomain } from '@/features/projects/custom-domains/settings/components/HasuraDomain';
import { RunServiceDomains } from '@/features/projects/custom-domains/settings/components/RunServiceDomains';
import { type ReactElement } from 'react';
export default function CustomDomains() {
const { currentProject } = useCurrentWorkspaceAndProject();
if (currentProject.plan.isFree) {
return (
<Container
className="grid grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<UpgradeToProBanner
title="Upgrade to Nhost Pro to unlock custom domains"
description="In publishing and graphic design, Lorem ipsum is a placeholder text
commonly used to demonstrate the visual form of a document or a
typeface without relying on meaningful content."
/>
</Container>
);
}
return (
<Container
className="grid max-w-5xl grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<Box className="flex flex-row items-center gap-4 overflow-hidden rounded-lg border-1 p-4">
<div className="flex flex-col space-y-2">
<Text className="text-lg font-semibold">Custom Domains</Text>
<Text color="secondary">
Add a custom domain to Auth, Hasura, PostgreSQL, and your Run
services for only a $10 flat fee 🚀 <br /> Learn more about
<Link
href="https://docs.nhost.io/platform/custom-domains"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="ml-1 font-medium"
>
Custom Domains
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
</div>
</Box>
<AuthDomain />
<HasuraDomain />
<DatabaseDomain />
<RunServiceDomains />
</Container>
);
}
CustomDomains.getLayout = function getLayout(page: ReactElement) {
return <SettingsLayout>{page}</SettingsLayout>;
};

View File

@@ -3,6 +3,7 @@ import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { DatabaseConnectionInfo } from '@/features/database/settings/components/DatabaseConnectionInfo';
import { DatabaseServiceVersionSettings } from '@/features/database/settings/components/DatabaseServiceVersionSettings';
import { DatabaseStorageCapacity } from '@/features/database/settings/components/DatabaseStorageCapacity';
import { ResetDatabasePasswordSettings } from '@/features/database/settings/components/ResetDatabasePasswordSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useGetPostgresSettingsQuery } from '@/generated/graphql';
@@ -36,6 +37,7 @@ export default function DatabaseSettingsPage() {
rootClassName="bg-transparent"
>
<DatabaseServiceVersionSettings />
<DatabaseStorageCapacity />
<DatabaseConnectionInfo />
<ResetDatabasePasswordSettings />
</Container>

View File

@@ -1,5 +1,6 @@
import { Container } from '@/components/layout/Container';
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { ProvidersUpdatedAlert } from '@/components/settings';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { AnonymousSignInSettings } from '@/features/authentication/settings/components/AnonymousSignInSettings';
import { AppleProviderSettings } from '@/features/authentication/settings/components/AppleProviderSettings';
@@ -54,6 +55,7 @@ export default function SettingsSignInMethodsPage() {
<WebAuthnSettings />
<AnonymousSignInSettings />
<SMSSettings />
{!currentProject.providersUpdated && <ProvidersUpdatedAlert />}
<AppleProviderSettings />
<AzureADProviderSettings />
<DiscordProviderSettings />

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
@@ -44,6 +45,7 @@ StoragePage.getLayout = function getLayout(page: ReactElement) {
<ProjectLayout
mainContainerProps={{ sx: { backgroundColor: 'background.default' } }}
>
<DepricationNotice />
{page}
</ProjectLayout>
);

View File

@@ -1,3 +1,4 @@
import DepricationNotice from '@/components/common/DepricationNotice/DepricationNotice';
import { useDialog } from '@/components/common/DialogProvider';
import { Pagination } from '@/components/common/Pagination';
import { Container } from '@/components/layout/Container';
@@ -390,6 +391,7 @@ export default function UsersPage() {
UsersPage.getLayout = function getLayout(page: ReactElement) {
return (
<ProjectLayout contentContainerProps={{ className: 'h-full' }}>
<DepricationNotice />
{page}
</ProjectLayout>
);

View File

@@ -1,4 +1,3 @@
import AnnouncementProvider from '@/components/common/Announcement/AnnouncementProvider';
import { DialogProvider } from '@/components/common/DialogProvider';
import { UIProvider } from '@/components/common/UIProvider';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
@@ -106,9 +105,7 @@ function MyApp({
>
<RetryableErrorBoundary>
<DialogProvider>
<AnnouncementProvider>
{getLayout(<Component {...pageProps} />)}
</AnnouncementProvider>
{getLayout(<Component {...pageProps} />)}
</DialogProvider>
</RetryableErrorBoundary>
</ThemeProvider>

View File

@@ -68,6 +68,7 @@ export const mockApplication: Project = {
name: 'Starter',
isFree: true,
price: 0,
featureMaxDbSize: 1,
},
config: {
observability: {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,21 @@
# @nhost/docs
## 0.7.0
### Minor Changes
- 5764f46d9: Add docs for Custom Domains
### Patch Changes
- cc8cc8d45: database: added extension http
## 0.6.2
### Patch Changes
- 4fe4a1696: return `refreshToken` immediately after signIn and signUp
## 0.6.1
### Patch Changes

View File

@@ -157,3 +157,28 @@ DROP EXTENSION pg_stat_statements;
### Resources
* [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
## http
HTTP client for PostgreSQL, retrieve a web page from inside the database.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION http;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION http;
```
### Resources
* [GitHub](https://github.com/pramsey/pgsql-http)

View File

@@ -0,0 +1,73 @@
---
title: 'Custom Domains'
sidebar_position: 10
image: /img/og/platform/metrics.png
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Custom domains empower you to offer a tailored and branded experience for your users. Available only as an add-on for projects on the pro and enterprise plans, custom domains not only enhance your brand's visibility but also provide a more professional appearance compared to using Nhost's default domain.
You can configure Custom Domains for Auth, Hasura, PostgreSQL, and your own Run services using both the Nhost Dashboard or the Config file.
The following examples assume we are configuring custom domains at `*.custom-domain.com`.
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
Follow the instructions in the **Custom Domain** section of your project's settings:
1. Add a CNAME record in your DNS provider for each of the services you want a custom domain for, and click "Verify". The verification might take a few seconds to succeed.
2. Once the verification succeeds, click "Save" to update your project.
![exposing a port](/img/custom-domains/custom-domains.png)
</TabItem>
<TabItem value="toml" label="toml">
The first step is to add a CNAME record in your DNS provider for each of the services you want a custom domain for. You can find the instructions in the **dashboard** tab.
For Hasura, Auth, and PostgreSQL, custom domains are defined in the default `./nhost/config.toml` as follows:
```
[hasura]
[hasura.resources.networking]
[[hasura.resources.networking.ingresses]]
fqdn = ['hasura.custom-domain.com']
[auth]
[auth.resources.networking]
[[auth.resources.networking.ingresses]]
fqdn = ['auth.custom-domain.com']
[postgres]
[postgres.resources.networking]
[[postgres.resources.networking.ingresses]]
fqdn = ['postgres.custom-domain.com']
```
For Run services, typically in `nhost-service.toml` specific to the service:
```
name = 'my-service'
[image]
image = 'docker.io/nhost/my-service'
[[ports]]
port = 8080
type= 'http'
publish = true
[[ports.ingresses]]
fqdn = ['my-service.custom-domain.com']
```
</TabItem>
</Tabs>

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.6.1",
"version": "0.7.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_NHOST_SUBDOMAIN=local
NEXT_PUBLIC_NHOST_REGION=

View File

@@ -0,0 +1,6 @@
module.exports = {
extends: ['../../config/.eslintrc.js', 'plugin:@next/next/recommended'],
rules: {
'react/react-in-jsx-scope': 'off'
}
}

View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,11 @@
# @nhost-examples/nextjs-server-components
## 0.1.0
### Minor Changes
- 4fe4a1696: new quickstart project that demonstrates how to use the Nhost SDK with Next.js 13 server components
### Patch Changes
- @nhost/nhost-js@2.2.17

View File

@@ -0,0 +1,70 @@
# Nhost with Next.js Server Components
This quickstart showcases how to correctly add authentication to a Next.js 13 project using the new App Router and Server Components. The other parts of the SDK (Storage / GraphQL/ Functions) should work the same as before.
## Authentication
1. **Saving the auth session**
To enable authentication with Server Components we have to store the auth session in a cookie. This should be done right after any **signIn** or **signUp** operation. See example [here](https://github.com/nhost/nhost/blob/main/examples/quickstarts/nextjs-server-components/src/app/server-actions/auth/sign-in-email-password.ts).
2. **Oauth & refresh session middleware**
Create a middleware at the root of your project that calls the helper method `manageAuthSession`. Feel free to copy paste the the contents of the `/utils` folder to your project. The second argument for `manageAuthSession` is for handling the case where there's an error refreshing the current session with the `refreshToken` stored in the cookie.
```typescript
import { manageAuthSession } from '@utils/nhost'
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
return manageAuthSession(request, () =>
NextResponse.redirect(new URL('/auth/sign-in', request.url))
)
}
```
3. **Protected routes**
To make sure only authenticated users access some Server Components, wrap them in the Higher Order Server Component `withAuthAsync`.
```typescript
import withAuthAsync from '@utils/auth-guard'
const MyProtectedServerComponent = async () => {
return <h2>Protected</h2>
}
export default withAuthAsync(MyProtectedServerComponent)
```
## Get Started
1. Clone the repository
```sh
git clone https://github.com/nhost/nhost
cd nhost
```
2. Install and build dependencies
```sh
pnpm install
pnpm build
```
3. Terminal 1: Start the Nhost Backend
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
```sh
cd examples/quickstarts/nhost-backend
nhost up
```
4. Terminal 2: Start the Next.js application
```sh
cd examples/quickstarts/nextjs-server-components
pnpm dev
```

View File

@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true
}
}
module.exports = nextConfig

View File

@@ -0,0 +1,35 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "^3.8.2",
"@nhost/nhost-js": "workspace:^",
"autoprefixer": "10.4.15",
"cookies-next": "^3.0.0",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"form-data": "^4.0.0",
"js-cookie": "^3.0.5",
"next": "13.4.19",
"postcss": "8.4.29",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwind-merge": "^1.8.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"xstate": "^4.38.2"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
"@types/node": "20.5.6",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,36 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { signIn } from '@server-actions/auth'
import { useState } from 'react'
export default function SignInWithEmailAndPassword() {
const [error, setError] = useState('')
async function handleSignIn(formData: FormData) {
const response = await signIn(formData)
if (response?.error) {
setError(response.error)
}
}
return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign in with email and password</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
<form className="w-full max-w-lg space-y-5" action={handleSignIn}>
<Input label="Email" id="email" name="email" type="email" required />
<Input label="Password" id="password" name="password" type="password" required />
<SubmitButton type="submit" className="w-full">
Sign in
</SubmitButton>
</form>
</div>
)
}

View File

@@ -0,0 +1,57 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { NhostClient } from '@nhost/nhost-js'
import { useState, type FormEvent } from 'react'
const nhost = new NhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN,
region: process.env.NEXT_PUBLIC_NHOST_REGION
})
export default function SignInMagickLink() {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [isSuccess, setIsSuccess] = useState(false)
const handleSignIn = async (e: FormEvent) => {
e.preventDefault()
const { error } = await nhost.auth.signIn({ email })
if (error) {
setError(error.message)
} else {
setIsSuccess(true)
}
}
return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign in with a magick link</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
{isSuccess && (
<p className="mt-3 font-semibold text-center text-green-500">
Click the link in the email to finish the sign in process
</p>
)}
<form className="w-full max-w-lg space-y-5" onSubmit={handleSignIn}>
<Input
label="Email"
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<SubmitButton type="submit" className="w-full">
Sign In
</SubmitButton>
</form>
</div>
)
}

View File

@@ -0,0 +1,67 @@
'use client'
import { signInWithGoogle } from '@server-actions/auth'
import { useRouter } from 'next/navigation'
export default function SignIn() {
const router = useRouter()
return (
<div className="container flex justify-center">
<div className="w-full max-w-lg space-y-5">
<h1 className="text-2xl font-semibold text-center">Sign In</h1>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/email-password')}
>
with email/password
</button>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/webauthn')}
>
with a security key
</button>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/magick-link')}
>
with a magick link
</button>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/pat')}
>
with a Personal Access Token
</button>
<button
type="button"
className="text-white w-full bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center justify-between dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
onClick={() => signInWithGoogle()}
>
<svg
className="w-4 h-4 mr-2 -ml-1"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
>
<path
fill="currentColor"
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
/>
</svg>
with Google <span />
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { signInWithPAT } from '@server-actions/auth'
import { useState } from 'react'
export default function SignInWithPAT() {
const [error, setError] = useState('')
async function handleSignIn(formData: FormData) {
const response = await signInWithPAT(formData)
if (response?.error) {
setError(response.error)
}
}
return (
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-semibold text-center">Sign In with Personal Access Token</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
<form className="w-full max-w-lg space-y-5" action={handleSignIn}>
<Input label="PAT" id="pat" name="pat" required />
<SubmitButton type="submit" className="w-full">
Sign In
</SubmitButton>
</form>
</div>
)
}

View File

@@ -0,0 +1,62 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { NhostClient } from '@nhost/nhost-js'
import Cookies from 'js-cookie'
import { useRouter } from 'next/navigation'
import { useState, type FormEvent } from 'react'
const NHOST_SESSION_KEY = 'nhostSession'
const nhost = new NhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN,
region: process.env.NEXT_PUBLIC_NHOST_REGION
})
export default function SignInWithSecurityKey() {
const router = useRouter()
const [error, setError] = useState('')
const [email, setEmail] = useState('')
const handleSignIn = async (e: FormEvent) => {
e.preventDefault()
const { session, error } = await nhost.auth.signIn({
email,
securityKey: true
})
if (error) {
setError(error.message)
}
if (session) {
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { sameSite: 'strict' })
router.push('/protected/todos')
}
}
return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign In</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
<form className="w-full max-w-lg space-y-5" onSubmit={handleSignIn}>
<Input
label="Email"
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<SubmitButton type="submit" className="w-full">
Sign In
</SubmitButton>
</form>
</div>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { signUp } from '@server-actions/auth'
import { useState } from 'react'
export default function SignUpWithEmailAndPassword() {
const [error, setError] = useState('')
async function handleSignUp(formData: FormData) {
const response = await signUp(formData)
if (response?.error) {
setError(response.error)
}
}
return (
<>
<h1 className="text-2xl font-semibold text-center">Sign Up</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
<form className="space-y-5" action={handleSignUp}>
<Input label="First Name" id="firstName" name="firstName" required />
<Input label="Last Name" id="lastName" name="lastName" required />
<Input label="Email" id="email" name="email" type="email" required />
<Input label="Password" id="password" name="password" type="password" required />
<SubmitButton type="submit">Sign Up</SubmitButton>
</form>
</>
)
}

View File

@@ -0,0 +1,53 @@
'use client'
import { signInWithGoogle } from '@server-actions/auth'
import { useRouter } from 'next/navigation'
export default function SignUp() {
const router = useRouter()
return (
<div className="container flex justify-center">
<div className="w-full max-w-lg space-y-5">
<h1 className="text-2xl font-semibold text-center">Sign Up</h1>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-up/email-password')}
>
with email/password
</button>
<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-up/webauthn')}
>
with a security key
</button>
<button
type="button"
className="text-white w-full bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center justify-between dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
onClick={() => signInWithGoogle()}
>
<svg
className="w-4 h-4 mr-2 -ml-1"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
>
<path
fill="currentColor"
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
/>
</svg>
with Google <span />
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,67 @@
'use client'
import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { NhostClient } from '@nhost/nhost-js'
import Cookies from 'js-cookie'
import { useRouter } from 'next/navigation'
import { useState, type FormEvent } from 'react'
const NHOST_SESSION_KEY = 'nhostSession'
const nhost = new NhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN,
region: process.env.NEXT_PUBLIC_NHOST_REGION
})
export default function SignUpWebAuthn() {
const router = useRouter()
const [error, setError] = useState('')
const [email, setEmail] = useState('')
const handleSignUp = async (e: FormEvent) => {
e.preventDefault()
const { session, error } = await nhost.auth.signUp({
email,
securityKey: true
})
if (error) {
setError(error.message)
}
console.log({
handleSignUpSession: session
})
if (session) {
Cookies.set(NHOST_SESSION_KEY, btoa(JSON.stringify(session)), { sameSite: 'strict' })
router.push('/protected/todos')
}
}
return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign Up with a security key</h1>
{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
<form className="w-full max-w-lg space-y-5" onSubmit={handleSignUp}>
<Input
label="Email"
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<SubmitButton type="submit" className="w-full">
Sign Up
</SubmitButton>
</form>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,24 @@
import Navigation from '@components/navigation'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<div className="app">
<Navigation />
<div className="container p-4 mx-auto mt-8 antialiased">{children}</div>
</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,3 @@
export default function Home() {
return <h1 className="text-2xl text-center">Hi, login/register to get started</h1>
}

View File

@@ -0,0 +1,19 @@
import withAuth from '@utils/auth-guard'
import { getNhost } from '@utils/nhost'
type EchoResponse = {
headers: Record<string, string>
}
const Echo = async () => {
const nhost = await getNhost()
const { res } = await nhost.functions.call<EchoResponse>('echo')
return (
<div>
<pre className="overflow-auto">{JSON.stringify(res?.data.headers, null, 2)}</pre>
</div>
)
}
export default withAuth(Echo)

Some files were not shown because too many files have changed in this diff Show More