Compare commits
209 Commits
@nhost/das
...
@nhost/nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55267c680e | ||
|
|
4d856f557f | ||
|
|
64c579cf8c | ||
|
|
eae65c715b | ||
|
|
9e69f9f235 | ||
|
|
8b127fbb62 | ||
|
|
86ba2081ec | ||
|
|
7c2c31082a | ||
|
|
60f705b033 | ||
|
|
ea34635eb2 | ||
|
|
2004687044 | ||
|
|
bd025d43ca | ||
|
|
87a05f7374 | ||
|
|
798f147db7 | ||
|
|
62b7fd2376 | ||
|
|
1ee021b4a3 | ||
|
|
6e61dce297 | ||
|
|
bd744e52dc | ||
|
|
85723d740b | ||
|
|
36e79e7b32 | ||
|
|
f61264b319 | ||
|
|
e84d9d2576 | ||
|
|
ea69d4f0f1 | ||
|
|
212d58bee5 | ||
|
|
c3d6b7beec | ||
|
|
5d5d8ef4f3 | ||
|
|
deb61fe97c | ||
|
|
04d36154b0 | ||
|
|
203cfb10b9 | ||
|
|
9690f871fa | ||
|
|
74a6b93971 | ||
|
|
dd4c0d2430 | ||
|
|
83f2ca5cde | ||
|
|
0c49e757c8 | ||
|
|
e90a9d7696 | ||
|
|
00a06466f5 | ||
|
|
8ca9f76cb2 | ||
|
|
78113dd62a | ||
|
|
adb0ee82c6 | ||
|
|
a41bb6cae6 | ||
|
|
1c59c363ee | ||
|
|
1d99f26fec | ||
|
|
49edb0e627 | ||
|
|
f011e71ae1 | ||
|
|
00c363f808 | ||
|
|
0b2f749ae9 | ||
|
|
cf62a1e6e3 | ||
|
|
8df84d782f | ||
|
|
f0deffafe1 | ||
|
|
a291da661d | ||
|
|
66c3193bc9 | ||
|
|
ac7be49cef | ||
|
|
fa79b77093 | ||
|
|
5823947933 | ||
|
|
333837fb57 | ||
|
|
7fae68f6cf | ||
|
|
f2751f4bac | ||
|
|
089acbbe70 | ||
|
|
6e08a82f49 | ||
|
|
6899ef3b39 | ||
|
|
cad3686364 | ||
|
|
8f2c002715 | ||
|
|
b70d61198f | ||
|
|
d29af2ce6f | ||
|
|
cdc992b888 | ||
|
|
205a20de87 | ||
|
|
b092b8fe08 | ||
|
|
2d40cbf624 | ||
|
|
7b591e8c4c | ||
|
|
72b425a5bc | ||
|
|
971ff92ab4 | ||
|
|
b7f801874d | ||
|
|
ff69f30e47 | ||
|
|
cc1932492d | ||
|
|
f45037e79f | ||
|
|
48658e2925 | ||
|
|
b90bb6b924 | ||
|
|
de61f45bd5 | ||
|
|
fd11e5ca2c | ||
|
|
7839c786ef | ||
|
|
a2bcd6a4b6 | ||
|
|
2cd5b26e0e | ||
|
|
559611af70 | ||
|
|
ffb45f5a49 | ||
|
|
451e80ac12 | ||
|
|
c9f8e523f2 | ||
|
|
331ba03768 | ||
|
|
611b26bc7d | ||
|
|
a446c3efca | ||
|
|
24424ae4dc | ||
|
|
2a5b705c26 | ||
|
|
7f3a32d386 | ||
|
|
11fa442aa8 | ||
|
|
5764f46d99 | ||
|
|
78d501801b | ||
|
|
cc8cc8d45d | ||
|
|
61fc83996b | ||
|
|
9ddb37e9bb | ||
|
|
262828f9a1 | ||
|
|
12f9726ad7 | ||
|
|
845937b552 | ||
|
|
f777a3380a | ||
|
|
5081372cab | ||
|
|
82212345c8 | ||
|
|
32d3f167c5 | ||
|
|
3d5f1ea922 | ||
|
|
97841ee5e8 | ||
|
|
4f3a615ebe | ||
|
|
8e8197691c | ||
|
|
e10389ecf6 | ||
|
|
cbdf6affec | ||
|
|
d19406e694 | ||
|
|
cffc5dc65b | ||
|
|
2b5cb58553 | ||
|
|
7459a9413e | ||
|
|
56871cc9f7 | ||
|
|
8f4d66e52d | ||
|
|
315a820073 | ||
|
|
ca57ad2cbd | ||
|
|
40259344eb | ||
|
|
4749f60a08 | ||
|
|
ac1888514d | ||
|
|
49b4af439b | ||
|
|
61e03d6c70 | ||
|
|
bec0fce497 | ||
|
|
c01568a7dd | ||
|
|
e934216a82 | ||
|
|
701d6b8c84 | ||
|
|
e158e2440a | ||
|
|
fbaa657001 | ||
|
|
559db6d0ec | ||
|
|
4c844930f1 | ||
|
|
3ef503ff81 | ||
|
|
bfcfd236ea | ||
|
|
bfa7033506 | ||
|
|
78c29fcf0e | ||
|
|
f1b934ed22 | ||
|
|
914369c53f | ||
|
|
af379b967e | ||
|
|
c3efb7ec84 | ||
|
|
27cbd48c8c | ||
|
|
236996a903 | ||
|
|
5d0936bb93 | ||
|
|
733c212f2d | ||
|
|
8b47549189 | ||
|
|
3c9c1025ce | ||
|
|
3e46d3873c | ||
|
|
4cf8820d72 | ||
|
|
02a11184fb | ||
|
|
7214d47cc7 | ||
|
|
238b77baad | ||
|
|
81b8e538b4 | ||
|
|
563a37e58d | ||
|
|
bff23720ee | ||
|
|
02cbaeffd2 | ||
|
|
9eb814c79a | ||
|
|
ebc5913bb3 | ||
|
|
4fe4a16964 | ||
|
|
92c475b7a7 | ||
|
|
679b34b031 | ||
|
|
d3186aefbd | ||
|
|
fdecac9d69 | ||
|
|
5077283028 | ||
|
|
f5f662aad1 | ||
|
|
735b779af7 | ||
|
|
4418d6abcf | ||
|
|
049e315c30 | ||
|
|
764597538b | ||
|
|
c8aea785cc | ||
|
|
e0e44b2ff4 | ||
|
|
12280f7c87 | ||
|
|
732a4f40ca | ||
|
|
d67fd599e4 | ||
|
|
a41231927a | ||
|
|
42ec665950 | ||
|
|
7225712a30 | ||
|
|
6593fdd9bb | ||
|
|
40039fece5 | ||
|
|
e5fcfb3cd5 | ||
|
|
218ec314fb | ||
|
|
9367e91d45 | ||
|
|
06c640be2c | ||
|
|
ae45be9816 | ||
|
|
ec4be590d8 | ||
|
|
5c51653aa0 | ||
|
|
7348c15ad1 | ||
|
|
44831e32a7 | ||
|
|
ee0f837762 | ||
|
|
e040979e91 | ||
|
|
68100d63b9 | ||
|
|
9b800046d7 | ||
|
|
807d8574b6 | ||
|
|
77028e4eef | ||
|
|
e0d32aab33 | ||
|
|
75c4c8ae36 | ||
|
|
1d90639e46 | ||
|
|
765b398b21 | ||
|
|
30aae1557c | ||
|
|
a3efc1d131 | ||
|
|
612d754965 | ||
|
|
b2e5f30379 | ||
|
|
3b3e83a218 | ||
|
|
0d5231f1a1 | ||
|
|
1a8332a3ca | ||
|
|
7418105de2 | ||
|
|
425d485f85 | ||
|
|
d8d25b3ea0 | ||
|
|
320513f6f5 | ||
|
|
e622ca0d83 |
@@ -5,6 +5,5 @@
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@nhost-examples/sveltekit"]
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
||||
64
.github/workflows/changesets.yaml
vendored
64
.github/workflows/changesets.yaml
vendored
@@ -42,6 +42,7 @@ jobs:
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm run release
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -62,12 +63,39 @@ jobs:
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- version
|
||||
- publish-vercel
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
@@ -113,42 +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 }}
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,5 +2,6 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
"eslint.workingDirectories": ["./dashboard"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,79 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.20.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7c2c31082: feat: add support for users to delete their account
|
||||
- @nhost/react-apollo@6.0.1
|
||||
- @nhost/nextjs@1.13.40
|
||||
|
||||
## 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
|
||||
|
||||
- 75c4c8ae3: feat(dashboard): make env value input multiline
|
||||
|
||||
## 0.20.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 425d485f8: fix(dashboard): make sure dedicated resources pricing follows total resources
|
||||
|
||||
## 0.20.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.17",
|
||||
"version": "0.20.28",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
BIN
dashboard/public/illustration-unbox.png
Normal file
BIN
dashboard/public/illustration-unbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Announcement';
|
||||
export * from './AnnouncementProvider';
|
||||
export { default as useAnnouncement } from './useAnnouncement';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ContactUs } from './DepricationNotice';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as UpgradeToProBanner } from './UpgradeToProBanner';
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -200,6 +200,14 @@ export default function SettingsSidebar({
|
||||
>
|
||||
Secrets
|
||||
</SettingsNavLink>
|
||||
|
||||
<SettingsNavLink
|
||||
href="/custom-domains"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Custom Domains
|
||||
</SettingsNavLink>
|
||||
</List>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
export { default as NhostIcon } from './NhostIcon';
|
||||
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal file
100
dashboard/src/components/settings/ProvidersUpdatedAlert.tsx
Normal 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'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'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>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/settings/index.ts
Normal file
1
dashboard/src/components/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ProvidersUpdatedAlert } from './ProvidersUpdatedAlert';
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ArrowsClockwise } from './ArrowsClockwise';
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Checkbox } from '@/components/ui/v2/Checkbox';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import {
|
||||
useDeleteUserAccountMutation,
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
import { useSignOut, useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
function ConfirmDeleteAccountModal({
|
||||
close,
|
||||
onDelete,
|
||||
}: {
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
|
||||
const user = useUserData();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const userHasProjects =
|
||||
!loading && data?.workspaces.some((workspace) => workspace.projects.length);
|
||||
|
||||
const userData = useUserData();
|
||||
|
||||
const [deleteUserAccount] = useDeleteUserAccountMutation({
|
||||
variables: { id: userData?.id },
|
||||
});
|
||||
|
||||
const onClickConfirm = async () => {
|
||||
setLoadingRemove(true);
|
||||
|
||||
await toast.promise(
|
||||
deleteUserAccount(),
|
||||
{
|
||||
loading: 'Deleting your account...',
|
||||
success: `The account has been deleted successfully.`,
|
||||
error: (arg: 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 deleting your account. Please try again.'
|
||||
);
|
||||
},
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
Delete Account?
|
||||
</Text>
|
||||
|
||||
{userHasProjects && (
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="font-bold"
|
||||
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
|
||||
>
|
||||
You still have active projects. Please delete your projects before
|
||||
proceeding with the account deletion.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box className="my-4">
|
||||
<Checkbox
|
||||
id="accept-1"
|
||||
label={`I'm sure I want to delete my account`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Project #1"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={onClickConfirm}
|
||||
disabled={userHasProjects}
|
||||
loading={loadingRemove}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DeleteAccount() {
|
||||
const router = useRouter();
|
||||
const { signOut } = useSignOut();
|
||||
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const onDelete = async () => {
|
||||
await signOut();
|
||||
await router.push('/signin');
|
||||
};
|
||||
|
||||
const confirmDeleteAccount = async () => {
|
||||
openDialog({
|
||||
component: (
|
||||
<ConfirmDeleteAccountModal close={closeDialog} onDelete={onDelete} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Delete Account"
|
||||
description="Please proceed with caution as the removal of your Personal Account and its contents from the Nhost platform is irreversible. This action will permanently delete your account and all associated data."
|
||||
className="px-0"
|
||||
slotProps={{
|
||||
submitButton: { className: 'hidden' },
|
||||
footer: { className: 'hidden' },
|
||||
}}
|
||||
>
|
||||
<Box className="grid grid-flow-row border-t-1">
|
||||
<Button
|
||||
color="error"
|
||||
className="mx-4 mt-4 justify-self-end"
|
||||
onClick={confirmDeleteAccount}
|
||||
>
|
||||
Delete Personal Account
|
||||
</Button>
|
||||
</Box>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteAccount } from './DeleteAccount';
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation deleteUserAccount($id: uuid!) {
|
||||
deleteUser(id: $id) {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,13 @@ query GetAuthenticationSettings($appId: uuid!) {
|
||||
expiresIn
|
||||
}
|
||||
}
|
||||
resources {
|
||||
networking {
|
||||
ingresses {
|
||||
fqdn
|
||||
}
|
||||
}
|
||||
}
|
||||
user {
|
||||
email {
|
||||
allowed
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DatabaseStorageCapacity } from './DatabaseStorageCapacity';
|
||||
@@ -9,6 +9,11 @@ query GetPostgresSettings($appId: uuid!) {
|
||||
__typename
|
||||
postgres {
|
||||
version
|
||||
resources {
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ query GetHasuraSettings($appId: uuid!) {
|
||||
events {
|
||||
httpPoolSize
|
||||
}
|
||||
resources {
|
||||
networking {
|
||||
ingresses {
|
||||
fqdn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Announcements } from './Announcements';
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ export default function useProjectRoutes() {
|
||||
exact: false,
|
||||
label: 'Run',
|
||||
icon: <ServicesIcon />,
|
||||
disabled: !isPlatform,
|
||||
},
|
||||
...nhostRoutes,
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AuthDomain } from './AuthDomain';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DatabaseDomain } from './DatabaseDomain';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HasuraDomain } from './HasuraDomain';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RunServiceDomains } from './RunServiceDomains';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RunServicePortDomain } from './RunServicePortDomain';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as VerifyDomain } from './VerifyDomain';
|
||||
@@ -128,6 +128,8 @@ export default function BaseEnvironmentVariableForm({
|
||||
error={!!errors.value}
|
||||
helperText={errors?.value?.message}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={5}
|
||||
autoComplete="off"
|
||||
autoFocus={mode === 'edit'}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
|
||||
import { Slider, sliderClasses } from '@/components/ui/v2/Slider';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
|
||||
import { calculateBillableResources } from '@/features/projects/resources/settings/utils/calculateBillableResources';
|
||||
import { getAllocatedResources } from '@/features/projects/resources/settings/utils/getAllocatedResources';
|
||||
import { prettifyMemory } from '@/features/projects/resources/settings/utils/prettifyMemory';
|
||||
import { prettifyVCPU } from '@/features/projects/resources/settings/utils/prettifyVCPU';
|
||||
@@ -63,34 +62,7 @@ export default function TotalResourcesFormFragment({
|
||||
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) *
|
||||
RESOURCE_VCPU_PRICE;
|
||||
|
||||
const billableResources = calculateBillableResources(
|
||||
{
|
||||
replicas: formValues.database?.replicas,
|
||||
vcpu: formValues.database?.vcpu,
|
||||
memory: formValues.database?.memory,
|
||||
},
|
||||
{
|
||||
replicas: formValues.hasura?.replicas,
|
||||
vcpu: formValues.hasura?.vcpu,
|
||||
memory: formValues.hasura?.memory,
|
||||
},
|
||||
{
|
||||
replicas: formValues.auth?.replicas,
|
||||
vcpu: formValues.auth?.vcpu,
|
||||
memory: formValues.auth?.memory,
|
||||
},
|
||||
{
|
||||
replicas: formValues.storage?.replicas,
|
||||
vcpu: formValues.storage?.vcpu,
|
||||
memory: formValues.storage?.memory,
|
||||
},
|
||||
);
|
||||
|
||||
const updatedPrice =
|
||||
Math.max(
|
||||
priceForTotalAvailableVCPU,
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
|
||||
) + proPlan.price;
|
||||
const updatedPrice = priceForTotalAvailableVCPU + proPlan.price;
|
||||
|
||||
const { vcpu: allocatedVCPU, memory: allocatedMemory } =
|
||||
getAllocatedResources(formValues);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
query dnsLookupCNAME($hostname: String!) {
|
||||
dnsLookupCNAME(hostname: $hostname)
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
mutation UpdateConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
|
||||
updateConfig(appID: $appId, config: $config) {
|
||||
id: __typename
|
||||
postgres {
|
||||
resources {
|
||||
storage {
|
||||
capacity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ fragment Project on apps {
|
||||
name
|
||||
price
|
||||
isFree
|
||||
featureMaxDbSize
|
||||
}
|
||||
githubRepository {
|
||||
fullName
|
||||
|
||||
14
dashboard/src/gql/platform/getAnnouncements.gql
Normal file
14
dashboard/src/gql/platform/getAnnouncements.gql
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,12 @@ query getRunServices(
|
||||
port
|
||||
type
|
||||
publish
|
||||
ingresses {
|
||||
fqdn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runServices_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
|
||||
5
dashboard/src/gql/settings/confirmProvidersUpdated.gql
Normal file
5
dashboard/src/gql/settings/confirmProvidersUpdated.gql
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation confirmProvidersUpdated($id: uuid!) {
|
||||
updateApp(pk_columns: { id: $id }, _set: { providersUpdated: true }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation insertFeedbackOne($feedback: feedback_insert_input!) {
|
||||
insertFeedbackOne(object: $feedback) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { AccountSettingsLayout } from '@/features/account/settings/components/AccountSettingsLayout';
|
||||
import { DeleteAccount } from '@/features/account/settings/components/DeleteAccount';
|
||||
import { PasswordSettings } from '@/features/account/settings/components/PasswordSettings';
|
||||
import { PATSettings } from '@/features/account/settings/components/PATSettings';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -18,6 +19,8 @@ export default function AccountSettingsPage() {
|
||||
<RetryableErrorBoundary>
|
||||
<PATSettings />
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<DeleteAccount />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export const mockApplication: Project = {
|
||||
name: 'Starter',
|
||||
isFree: true,
|
||||
price: 0,
|
||||
featureMaxDbSize: 1,
|
||||
},
|
||||
config: {
|
||||
observability: {
|
||||
|
||||
1916
dashboard/src/utils/__generated__/graphql.ts
generated
1916
dashboard/src/utils/__generated__/graphql.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,37 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1ee021b4a: remove custom domains from roadmap
|
||||
|
||||
## 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
|
||||
|
||||
- 612d75496: updated postgres and graphql documentation
|
||||
- 3b3e83a21: fix(docs): correct rendering of mermaid diagrams in dark mode
|
||||
- 765b398b2: added jit settings documentation
|
||||
- 30aae1557: minor fix to performance documentation
|
||||
- a3efc1d13: docs: added storage/antivirus documentation
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
184
docs/docs/database/extensions.mdx
Normal file
184
docs/docs/database/extensions.mdx
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
title: 'Extensions'
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
## postgis
|
||||
|
||||
PostGIS extends the capabilities of the PostgreSQL relational database by adding support storing, indexing and querying geographic data.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION postgis;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION postgis;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [Official website](https://postgis.net)
|
||||
|
||||
## pgvector
|
||||
|
||||
Open-source vector similarity search for Postgres. Store your vectors with the rest of your data. Supports:
|
||||
|
||||
* exact and approximate nearest neighbor search
|
||||
* L2 distance, inner product, and cosine distance
|
||||
* any language with a Postgres client
|
||||
|
||||
Plus ACID compliance, point-in-time recovery, JOINs, and all of the other great features of Postgres
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION vector;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION vector;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [GitHub](https://github.com/pgvector/pgvector)
|
||||
|
||||
## pg_cron
|
||||
|
||||
pg_cron is a simple cron-based job scheduler for PostgreSQL (10 or higher) that runs inside the database as an extension. It uses the same syntax as regular cron, but it allows you to schedule PostgreSQL commands directly from the database. You can also use '[1-59] seconds' to schedule a job based on an interval.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_cron;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION pg_cron;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [GitHub](https://github.com/citusdata/pg_cron)
|
||||
|
||||
## hypopg
|
||||
|
||||
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
|
||||
|
||||
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION hypopg;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION hypopg;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [GitHub](https://github.com/HypoPG/hypopg)
|
||||
* [Documentation](https://hypopg.readthedocs.io)
|
||||
|
||||
## timescaledb
|
||||
|
||||
TimescaleDB is an open-source database designed to make SQL scalable for time-series data. It is engineered up from PostgreSQL and packaged as a PostgreSQL extension, providing automatic partitioning across time and space (partitioning key), as well as full SQL support.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION timescaledb;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION timescaledb;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [GitHub](https://github.com/timescale/timescaledb)
|
||||
* [Documentation](https://docs.timescale.com)
|
||||
* [Website](https://www.timescale.com)
|
||||
|
||||
## pg_stat_statements
|
||||
|
||||
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION pg_stat_statements;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
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)
|
||||
89
docs/docs/database/performance.mdx
Normal file
89
docs/docs/database/performance.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: 'Performance'
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
Ensuring a healthy and performant PostgreSQL service is crucial as it directly impacts the overall response time and stability of your backend. Since Postgres serves as the centerpiece of your backend, prioritize the optimization and maintenance of your Postgres service to achieve the desired performance and reliability.
|
||||
|
||||
In case your Postgres service is not meeting your performance expectations, you can explore the following options:
|
||||
|
||||
1. Consider upgrading your [dedicated compute](/platform/compute) resources to provide more processing power and memory to the Postgres server.
|
||||
|
||||
2. Fine-tune the configuration parameters of Postgres to optimize its performance. Adjust settings such as `shared_buffers`, `work_mem`, and `effective_cache_size` to better align with your workload and server resources.
|
||||
|
||||
3. Identify and analyze slow-performing queries using tools like query logs or query monitoring extensions. Optimize or rewrite these queries to improve their efficiency.
|
||||
|
||||
4. Evaluate the usage of indexes in your database. Identify queries that could benefit from additional indexes and strategically add them to improve query performance.
|
||||
|
||||
By implementing these steps, you can effectively address performance concerns and enhance the overall performance of your Postgres service.
|
||||
|
||||
## Upgrade to our latest postgres image
|
||||
|
||||
Before trying anything else, always upgrade to our latest postgres image first. You can find our availables images in the dashbhoard, under your database settings.
|
||||
|
||||
## Upgrading dedicated compute
|
||||
|
||||
Increasing CPU and memory is the simplest way to address performance issues. You can read more about compute resources [here](/platform/compute).
|
||||
|
||||
## Fine-tune configuration parameters
|
||||
|
||||
When optimizing your Postgres setup, you can consider adjusting various Postgres settings. You can find a list of these parameters [here](/database/settings). Keep in mind that the optimal values for these parameters will depend on factors such as available resources, workload, and data distribution.
|
||||
|
||||
To help you get started, you can use [pgtune](https://pgtune.leopard.in.ua) as a reference tool. Pgtune can generate recommended configuration settings based on your system specifications. By providing information about your system, it can suggest parameter values that may be a good starting point for optimization.
|
||||
|
||||
However, it's important to note that the generated settings from pgtune are not guaranteed to be the best for your specific environment. It's always recommended to review and customize the suggested settings based on your particular requirements, performance testing, and ongoing monitoring of your Postgres database.
|
||||
|
||||
## Identifying slow queries
|
||||
|
||||
Monitoring slow queries is a highly effective method for tackling performance issues. Several tools leverage [pg_stat_statements](https://www.postgresql.org/docs/14/pgstatstatements.html), a PostgreSQL extension, to provide constant monitoring. You can employ these tools to identify and address slow queries in real-time.
|
||||
|
||||
### pghero
|
||||
|
||||
[PgHero](https://github.com/ankane/pghero) is one of such tools you can use to idenfity and address slow queries. You can easily run pghero alongside your postgres with [Nhost Run](/run):
|
||||
|
||||
1. First, make sure the extension [pg_stat_statements](/database/extensions#pg_stat_statements) is enabled.
|
||||
|
||||
2. Click on this [one-click install link](https://app.nhost.io:/run-one-click-install?config=eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19)
|
||||
|
||||
3. Select your project:
|
||||

|
||||
|
||||
4. Replace the placeholders with your postgres password, subdomain and a user and password to protect your pghero service. Finally, click on create.
|
||||

|
||||
|
||||
5. After confirming the service, copy the URL:
|
||||

|
||||
|
||||
6. Finally, you can open the link you just copied to access pghero:
|
||||
|
||||

|
||||
|
||||
|
||||
:::info
|
||||
When you create a new service, it can take a few minutes for the DNS (Domain Name System) to propagate. If your browser displays an error stating that it couldn't find the server or website, simply wait for a couple of minutes and then try again.
|
||||
:::
|
||||
|
||||
After successfully setting up pghero, it will begin displaying slow queries, suggesting index proposals, and offering other valuable information. Utilize this data to enhance your service's performance.
|
||||
|
||||
## Adding indexes
|
||||
|
||||
Indexes can significantly enhance the speed of data retrieval. However, it's essential to be aware that they introduce additional overhead during mutations. Therefore, understanding your workload is crucial before opting to add an index.
|
||||
|
||||
There are tools you can use to help analyze your workload and detect missing indexes.
|
||||
|
||||
### pghero
|
||||
|
||||
[PgHero](https://github.com/ankane/pghero), in addition to help with slow queries, can also help finding missing and duplicate indexes. See previous section on how to deploy pghero with [Nhost Run](/run).
|
||||
|
||||
### dexter
|
||||
|
||||
[Dexter](https://github.com/ankane/dexter) can leverage both [pg_stat_statements](https://www.postgresql.org/docs/14/pgstatstatements.html) and [hypopg](https://hypopg.readthedocs.io/en/rel1_stable/) to find and evaluate indexes. You can run dexter directly from your machine:
|
||||
|
||||
1. Enable [hypopg](/database/extensions#hypopg)
|
||||
2. Execute the command `docker run --rm -it ankane/dexter [POSTGRES_CONN_STRING] --pg-stat-statements`
|
||||
|
||||
```
|
||||
$ docker run --rm -it ankane/dexter [POSTGRES_CONN_STRING] --pg-stat-statements
|
||||
Processing 1631 new query fingerprints
|
||||
No new indexes found
|
||||
```
|
||||
84
docs/docs/database/settings.mdx
Normal file
84
docs/docs/database/settings.mdx
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 'Settings'
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
Below you can find the official schema (cue) and an example to configure your postgres database:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="schema" label="schema">
|
||||
|
||||
```cue
|
||||
#Postgres: {
|
||||
version: string | *"14.6-20230705-1"
|
||||
|
||||
// Resources for the service, optional
|
||||
resources?: #Resources & {
|
||||
replicas: 1
|
||||
}
|
||||
|
||||
// postgres settings of the same name in camelCase, optional
|
||||
settings?: {
|
||||
jit: "off" | "on" | *"on"
|
||||
maxConnections: int32 | *100
|
||||
sharedBuffers: string | *"128MB"
|
||||
effectiveCacheSize: string | *"4GB"
|
||||
maintenanceWorkMem: string | *"64MB"
|
||||
checkpointCompletionTarget: number | *0.9
|
||||
walBuffers: int32 | *-1
|
||||
defaultStatisticsTarget: int32 | *100
|
||||
randomPageCost: number | *4.0
|
||||
effectiveIOConcurrency: int32 | *1
|
||||
workMem: string | *"4MB"
|
||||
hugePages: string | *"try"
|
||||
minWalSize: string | *"80MB"
|
||||
maxWalSize: string | *"1GB"
|
||||
maxWorkerProcesses: int32 | *8
|
||||
maxParallelWorkersPerGather: int32 | *2
|
||||
maxParallelWorkers: int32 | *8
|
||||
maxParallelMaintenanceWorkers: int32 | *2
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml" default>
|
||||
|
||||
```toml
|
||||
[postgres]
|
||||
version = '14.6-20230925-1'
|
||||
|
||||
[postgres.resources.compute]
|
||||
cpu = 1000
|
||||
memory = 2048
|
||||
|
||||
[postgres.settings]
|
||||
jit = "off"
|
||||
maxConnections = 100
|
||||
sharedBuffers = '256MB'
|
||||
effectiveCacheSize = '768MB'
|
||||
maintenanceWorkMem = '64MB'
|
||||
checkpointCompletionTarget = 0.9
|
||||
walBuffers = -1
|
||||
defaultStatisticsTarget = 100
|
||||
randomPageCost = 1.1
|
||||
effectiveIOConcurrency = 200
|
||||
workMem = '1310kB'
|
||||
hugePages = 'off'
|
||||
minWalSize = '80MB'
|
||||
maxWalSize = '1GB'
|
||||
maxWorkerProcesses = 8
|
||||
maxParallelWorkersPerGather = 2
|
||||
maxParallelWorkers = 8
|
||||
maxParallelMaintenanceWorkers = 2
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
:::info
|
||||
At the time of writing this document postgres settings are only supported via the [configuration file](https://nhost.io/blog/config).
|
||||
:::
|
||||
@@ -144,6 +144,14 @@ One of the most common permission requirements is that authenticated users shoul
|
||||
1. Select the **columns** you want the user to be able to read. In our case, we'll allow the user to read all columns.
|
||||
1. Click **Save**.
|
||||
|
||||
## Known issues
|
||||
|
||||
### Permissions are slow
|
||||
|
||||
In certain situations, permission checks can cause significant delays. One way to identify this issue is by comparing the execution time of a GraphQL query when performed as an admin versus as a regular user. To resolve such cases, disabling the Just-in-Time (JIT) compilation in [Postgres](/database/settings) can be beneficial.
|
||||
|
||||
[Github issue](https://github.com/hasura/graphql-engine/issues/3672)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Hasura has more in-depth documentation related to permissions that you can learn from:
|
||||
|
||||
174
docs/docs/graphql/settings.mdx
Normal file
174
docs/docs/graphql/settings.mdx
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: 'Settings'
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
Below you can find the official schema (cue) and an example to configure your graphql service:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="schema" label="schema">
|
||||
|
||||
```cue
|
||||
// Configuration for hasura service
|
||||
#Hasura: {
|
||||
// Version of hasura, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/hasura/graphql-engine/tags
|
||||
version: string | *"v2.33.4-ce"
|
||||
|
||||
// JWT Secrets configuration
|
||||
jwtSecrets: [#JWTSecret]
|
||||
|
||||
// Admin secret
|
||||
adminSecret: string
|
||||
|
||||
// Webhook secret
|
||||
webhookSecret: string
|
||||
|
||||
// Configuration for hasura services
|
||||
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
|
||||
settings: {
|
||||
// HASURA_GRAPHQL_CORS_DOMAIN
|
||||
corsDomain: [...#Url] | *["*"]
|
||||
// HASURA_GRAPHQL_DEV_MODE
|
||||
devMode: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_ALLOWLIST
|
||||
enableAllowList: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLE_CONSOLE
|
||||
enableConsole: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
|
||||
enableRemoteSchemaPermissions: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLED_APIS
|
||||
enabledAPIs: [...#HasuraAPIs] | *["metadata", "graphql", "pgdump", "config"]
|
||||
|
||||
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
|
||||
liveQueriesMultiplexedRefetchInterval: uint32 | *1000
|
||||
}
|
||||
|
||||
logs: {
|
||||
// HASURA_GRAPHQL_LOG_LEVEL
|
||||
level: "debug" | "info" | "error" | *"warn"
|
||||
}
|
||||
|
||||
events: {
|
||||
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
|
||||
httpPoolSize: uint32 & >=1 & <=100 | *100
|
||||
}
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml" default>
|
||||
|
||||
```toml
|
||||
[hasura]
|
||||
version = ''
|
||||
adminSecret = 'adminsecret'
|
||||
webhookSecret = 'webhooksecret'
|
||||
|
||||
[[hasura.jwtSecrets]]
|
||||
type = 'HS256'
|
||||
key = 'secret'
|
||||
|
||||
[hasura.settings]
|
||||
corsDomain = ['*']
|
||||
devMode = false
|
||||
enableAllowList = true
|
||||
enableConsole = true
|
||||
enableRemoteSchemaPermissions = true
|
||||
enabledAPIs = ['metadata']
|
||||
liveQueriesMultiplexedRefetchInterval = 1000
|
||||
|
||||
[hasura.logs]
|
||||
level = 'warn'
|
||||
|
||||
[hasura.events]
|
||||
httpPoolSize = 10
|
||||
|
||||
[hasura.resources]
|
||||
replicas = 1
|
||||
|
||||
[hasura.resources.compute]
|
||||
cpu = 500
|
||||
memory = 1024
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### JWT Secret
|
||||
|
||||
All formats supported by [hasura](https://hasura.io/docs/latest/auth/authentication/jwt/) should be supported:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="schema" label="schema" default>
|
||||
|
||||
```cue
|
||||
#JWTSecret:
|
||||
({
|
||||
type: "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "Ed25519" | *"HS256"
|
||||
key: string
|
||||
} |
|
||||
{
|
||||
jwk_url: #Url | *null
|
||||
}) &
|
||||
{
|
||||
claims_format?: "stringified_json" | *"json"
|
||||
audience?: string
|
||||
issuer?: string
|
||||
allowed_skew?: uint32
|
||||
header?: string
|
||||
} & {
|
||||
claims_map?: [...#ClaimMap]
|
||||
|
||||
} &
|
||||
({
|
||||
claims_namespace: string | *"https://hasura.io/jwt/claims"
|
||||
} |
|
||||
{
|
||||
claims_namespace_path: string
|
||||
} | *{})
|
||||
|
||||
#ClaimMap: {
|
||||
claim: string
|
||||
{
|
||||
value: string
|
||||
} | {
|
||||
path: string
|
||||
default?: string
|
||||
}
|
||||
} & {
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```toml
|
||||
# example 1
|
||||
[[hasura.jwtSecrets]]
|
||||
type = 'HS256'
|
||||
key = 'secret'
|
||||
|
||||
# example 2
|
||||
[[hasura.jwtSecrets]]
|
||||
jwk_url = 'https:/....'
|
||||
|
||||
# example 3
|
||||
[[hasura.jwtSecrets]]
|
||||
jwk_url = "https://......"
|
||||
issuer = "https://my-auth-server.com"
|
||||
|
||||
[[hasura.jwtSecrets.claims_map]]
|
||||
claim = "x-some-claim"
|
||||
value = "some-value"
|
||||
|
||||
[[hasura.jwtSecrets.claims_map]]
|
||||
claim = "x-other-claim"
|
||||
path = "$.user.claim.id"
|
||||
default = "default-value"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
73
docs/docs/platform/custom-domains.mdx
Normal file
73
docs/docs/platform/custom-domains.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||
@@ -43,7 +43,6 @@ Nhost Run works with container images built for the **arm architecture**. Images
|
||||
|
||||
Some missing functionality we are currently working on and should be added soon:
|
||||
|
||||
1. Custom domains
|
||||
2. Run services with the CLI alongside your project
|
||||
3. Ability to connect services to repositories for automated building and deployment (currently this needs to be done via a third party CI, see [Deployment via CI](/run/ci) for more details).
|
||||
4. Expose TCP/UDP ports
|
||||
1. Run services with the CLI alongside your project
|
||||
2. Ability to connect services to repositories for automated building and deployment (currently this needs to be done via a third party CI, see [Deployment via CI](/run/ci) for more details).
|
||||
3. Expose TCP/UDP ports
|
||||
|
||||
4
docs/docs/storage/_category_.json
Normal file
4
docs/docs/storage/_category_.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Storage",
|
||||
"position": 7
|
||||
}
|
||||
44
docs/docs/storage/av.mdx
Normal file
44
docs/docs/storage/av.mdx
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Antivirus
|
||||
sidebar_label: Antivirus
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
Integration with [clamav](https://www.clamav.net) antivirus relies on an external [clamd](https://docs.clamav.net/manual/Usage/Scanning.html#clamd) service. When a file is uploaded `hasura-storage` will create the file metadata first and then check if the file is clean with `clamd` via its TCP socket. If the file is clean the rest of the process will continue as usual. If a virus is found details about the virus will be added to the `virus` table and the rest of the process will be aborted.
|
||||
|
||||
``` mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
User ->> storage: upload file
|
||||
storage ->>clamav: check for virus
|
||||
alt virus found
|
||||
storage-->s3: abort upload
|
||||
storage->>graphql: insert row in virus table
|
||||
else virus not found
|
||||
storage->>s3: upload
|
||||
storage->>graphql: update metadata
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
To enable the antivirus you need to follow the next steps:
|
||||
|
||||
|
||||
1. Deploy using [Nhost Run](/run) a dedicated instance of `clamd` with this [one-click install link](https://app.nhost.io:/run-one-click-install?config=eyJuYW1lIjoiY2xhbWF2IiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vbmhvc3QvY2xhbWF2OjAuMS4xIn0sImNvbW1hbmQiOltdLCJyZXNvdXJjZXMiOnsiY29tcHV0ZSI6eyJjcHUiOjEwMDAsIm1lbW9yeSI6MjA0OH0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbXSwicG9ydHMiOlt7InBvcnQiOiIzMzEwIiwidHlwZSI6InRjcCIsInB1Ymxpc2giOmZhbHNlfV19).
|
||||
2. Select the project:
|
||||

|
||||
3. Click on "Create":
|
||||

|
||||
4. Make sure you are running **at least** storage version 0.4.0 and enable the antivirus:
|
||||

|
||||
5. Wait for the service to update and try to upload a sample virus file like [eicar](https://www.eicar.org/download-anti-malware-testfile/)
|
||||

|
||||
6. If the setup is working the upload should fail
|
||||

|
||||
7. You can also head to hasura and verify entries were added to the `virus` table:
|
||||

|
||||
|
||||
That entry should have useful information about like the filename, the virus found and the user session. In addition, the information on that table can be used a source for events.
|
||||
57
docs/docs/storage/example_crm.mdx
Normal file
57
docs/docs/storage/example_crm.mdx
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Example: CRM System"
|
||||
sidebar_label: "Example: CRM System"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
Let's say you want to build a CRM system and you want to store files for customers. This is one way how you could do that.
|
||||
|
||||
Start with, you would have two tables:
|
||||
|
||||
1. `customers` - Customer data.
|
||||
2. `customer_files` - What file belongs to what customer
|
||||
|
||||
```text
|
||||
- customers
|
||||
- id
|
||||
- name
|
||||
- address
|
||||
|
||||
customer_files
|
||||
- id
|
||||
- customer_id (Foreign Key to `customers.id`)
|
||||
- file_id (Foreign Key to `storage.files.id`)
|
||||
```
|
||||
|
||||
You would also create a [Hasura Relationship](https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/table-relationships/index/) (GraphQL relationship) between between `customers` and `customer_files` and between `customer_files` and `storage.files`.
|
||||
|
||||
With the two tables and GraphQL relationships in place, you can query customers and the customer's files like this:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
customers {
|
||||
# customers table
|
||||
id
|
||||
name
|
||||
customer_files {
|
||||
# customer_files table
|
||||
id
|
||||
file {
|
||||
# storage.files table
|
||||
id
|
||||
name
|
||||
size
|
||||
mimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The file upload process would be as follows:
|
||||
|
||||
1. Upload a file.
|
||||
2. Get the returned file id.
|
||||
3. Insert (GraphQL Mutation) the file `id` and the customer's `id` into the `customer_files` table.
|
||||
|
||||
This would allow you to upload and download files belonging to specific customers in your CRM system.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: 'Storage'
|
||||
sidebar_position: 7
|
||||
title: 'Overview'
|
||||
image: /img/og/storage.png
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
@@ -195,57 +195,3 @@ Image Transformation works on both public and pre-signed URLs.
|
||||
```text
|
||||
https://[subdomain].storage.[region].nhost.run/v1/files/08e6fa32-0880-4d0e-a832-278198acb363?w=500
|
||||
```
|
||||
|
||||
## Example: CRM System
|
||||
|
||||
Let's say you want to build a CRM system and you want to store files for customers. This is one way how you could do that.
|
||||
|
||||
Start with, you would have two tables:
|
||||
|
||||
1. `customers` - Customer data.
|
||||
2. `customer_files` - What file belongs to what customer
|
||||
|
||||
```text
|
||||
- customers
|
||||
- id
|
||||
- name
|
||||
- address
|
||||
|
||||
customer_files
|
||||
- id
|
||||
- customer_id (Foreign Key to `customers.id`)
|
||||
- file_id (Foreign Key to `storage.files.id`)
|
||||
```
|
||||
|
||||
You would also create a [Hasura Relationship](https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/table-relationships/index/) (GraphQL relationship) between between `customers` and `customer_files` and between `customer_files` and `storage.files`.
|
||||
|
||||
With the two tables and GraphQL relationships in place, you can query customers and the customer's files like this:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
customers {
|
||||
# customers table
|
||||
id
|
||||
name
|
||||
customer_files {
|
||||
# customer_files table
|
||||
id
|
||||
file {
|
||||
# storage.files table
|
||||
id
|
||||
name
|
||||
size
|
||||
mimeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The file upload process would be as follows:
|
||||
|
||||
1. Upload a file.
|
||||
2. Get the returned file id.
|
||||
3. Insert (GraphQL Mutation) the file `id` and the customer's `id` into the `customer_files` table.
|
||||
|
||||
This would allow you to upload and download files belonging to specific customers in your CRM system.
|
||||
@@ -26,6 +26,10 @@ const config = {
|
||||
favicon: 'img/favicon.png',
|
||||
organizationName: 'nhost',
|
||||
projectName: 'docs',
|
||||
markdown: {
|
||||
mermaid: true
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
scripts: [
|
||||
{ src: 'https://plausible.io/js/script.js', defer: true, 'data-domain': 'docs.nhost.io' }
|
||||
],
|
||||
@@ -44,7 +48,6 @@ const config = {
|
||||
routeBasePath: '/',
|
||||
breadcrumbs: false,
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
remarkPlugins: [require('mdx-mermaid')],
|
||||
editUrl: 'https://github.com/nhost/nhost/edit/main/docs/'
|
||||
},
|
||||
theme: {
|
||||
@@ -177,6 +180,7 @@ const config = {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
defaultLanguage: 'javascript',
|
||||
additionalLanguages: ['cue', 'toml'],
|
||||
magicComments: [
|
||||
{
|
||||
className: 'code-block-error-line',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
@@ -19,12 +19,13 @@
|
||||
"@docusaurus/core": "2.4.1",
|
||||
"@docusaurus/plugin-sitemap": "2.4.1",
|
||||
"@docusaurus/preset-classic": "2.4.1",
|
||||
"@docusaurus/theme-mermaid": "2.4.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-plugin-image-zoom": "^0.1.1",
|
||||
"mdx-mermaid": "^1.3.2",
|
||||
"mermaid": "^9.0.0",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"unist-util-visit": "^2.0.0"
|
||||
|
||||
@@ -44,7 +44,16 @@ const sidebars = {
|
||||
}
|
||||
]
|
||||
},
|
||||
'storage',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Storage',
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'storage'
|
||||
}
|
||||
]
|
||||
},
|
||||
'serverless-functions',
|
||||
{
|
||||
type: 'category',
|
||||
|
||||
2
docs/src/theme/prism-include-languages.d.ts
vendored
Normal file
2
docs/src/theme/prism-include-languages.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import type * as PrismNamespace from 'prismjs';
|
||||
export default function prismIncludeLanguages(PrismObject: typeof PrismNamespace): void;
|
||||
20
docs/src/theme/prism-include-languages.js
Normal file
20
docs/src/theme/prism-include-languages.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/*global globalThis*/
|
||||
import siteConfig from '@generated/docusaurus.config'
|
||||
export default function prismIncludeLanguages(PrismObject) {
|
||||
const {
|
||||
themeConfig: { prism }
|
||||
} = siteConfig
|
||||
const { additionalLanguages } = prism
|
||||
// Prism components work on the Prism instance on the window, while prism-
|
||||
// react-renderer uses its own Prism instance. We temporarily mount the
|
||||
// instance onto window, import components to enhance it, then remove it to
|
||||
// avoid polluting global namespace.
|
||||
// You can mutate PrismObject: registering plugins, deleting languages... As
|
||||
// long as you don't re-assign it
|
||||
globalThis.Prism = PrismObject
|
||||
additionalLanguages.forEach((lang) => {
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
require(`prismjs/components/prism-${lang}`)
|
||||
})
|
||||
delete globalThis.Prism
|
||||
}
|
||||
BIN
docs/static/img/custom-domains/custom-domains.png
vendored
Normal file
BIN
docs/static/img/custom-domains/custom-domains.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
BIN
docs/static/img/database/performance/pghero_01.png
vendored
Normal file
BIN
docs/static/img/database/performance/pghero_01.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/static/img/database/performance/pghero_02.png
vendored
Normal file
BIN
docs/static/img/database/performance/pghero_02.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/static/img/database/performance/pghero_03.png
vendored
Normal file
BIN
docs/static/img/database/performance/pghero_03.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
docs/static/img/database/performance/pghero_04.png
vendored
Normal file
BIN
docs/static/img/database/performance/pghero_04.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user