Compare commits
152 Commits
@nhost/rea
...
@nhost/apo
| 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 |
@@ -5,6 +5,5 @@
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["@nhost-examples/sveltekit"]
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
||||
12
.github/workflows/changesets.yaml
vendored
12
.github/workflows/changesets.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm run release
|
||||
createGithubReleases: true
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -141,16 +141,6 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
- name: Create GitHub Release
|
||||
uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
changelog: dashboard/CHANGELOG.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
|
||||
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,60 @@
|
||||
# @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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.20",
|
||||
"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';
|
||||
@@ -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,21 @@
|
||||
# @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
|
||||
|
||||
@@ -157,3 +157,28 @@ DROP EXTENSION pg_stat_statements;
|
||||
### Resources
|
||||
|
||||
* [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)
|
||||
|
||||
|
||||
## http
|
||||
|
||||
HTTP client for PostgreSQL, retrieve a web page from inside the database.
|
||||
|
||||
### Managing
|
||||
|
||||
To install the extension you can create a migration with the following contents:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
CREATE EXTENSION http;
|
||||
```
|
||||
|
||||
To uninstall it, you can use the following migration:
|
||||
|
||||
```
|
||||
SET ROLE postgres;
|
||||
DROP EXTENSION http;
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
* [GitHub](https://github.com/pramsey/pgsql-http)
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
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 |
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.20.1'
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/nextjs-server-components
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8b127fbb6]
|
||||
- @nhost/nhost-js@2.2.18
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs-server-components",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -25,7 +25,7 @@ httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 16
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.21.3'
|
||||
@@ -144,7 +144,7 @@ version = '14.6-20230406-2'
|
||||
[provider]
|
||||
|
||||
[storage]
|
||||
version = '0.3.5'
|
||||
version = '0.4.0'
|
||||
|
||||
[observability]
|
||||
[observability.grafana]
|
||||
|
||||
10
examples/quickstarts/sveltekit/CHANGELOG.md
Normal file
10
examples/quickstarts/sveltekit/CHANGELOG.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
|
||||
## 0.2.0
|
||||
### Minor Changes
|
||||
|
||||
- 4749f60a0: updated the SvelteKit quickstart project to demonstrate how to use the Nhost SDK on the server
|
||||
'@nhost-examples/sveltekit': minor
|
||||
---
|
||||
|
||||
feat: add nhost with sveltekit example project
|
||||
87
examples/quickstarts/sveltekit/README.md
Normal file
87
examples/quickstarts/sveltekit/README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Using Nhost with SvelteKit quickstart
|
||||
|
||||
This is a quickstart that showcases how to use the Nhost SDK running on server `load` functions and API routes that run exclusively on the server.
|
||||
|
||||
## Authentication
|
||||
|
||||
1. **Saving the auth session**
|
||||
|
||||
In order to utilize the Nhost SDK with an authenticated session on the server, it is necessary to securely store this session within a cookie. This should be done right after any **signIn** or **signUp** operation. See example [here](https://github.com/nhost/nhost/blob/main/examples/quickstarts/sveltekit/src/routes/auth/sign-in/email-password/+page.server.js).
|
||||
|
||||
2. **Oauth & refresh session server hook**
|
||||
|
||||
Create a server hook `src/hook.server.js` that calls the helper method `manageAuthSession`. Feel free to copy paste the `src/lib/nhost.js` to your project. The second argument for `manageAuthSession` is for handling the case where there's an error refreshing the current session with the `refreshToken` stored in the cookie.
|
||||
|
||||
```typescript
|
||||
import { manageAuthSession } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
await manageAuthSession(event, () => {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
})
|
||||
|
||||
return resolve(event)
|
||||
}
|
||||
```
|
||||
|
||||
3. **Protected routes**
|
||||
|
||||
To make sure only authenticated users access certain pages, you need to create a server root layout file `src/routes/+layout.server.js`. Within that file you define your public routes, so any other route would redirect if there's no session.
|
||||
|
||||
```typescript
|
||||
import { getNhost } from '$lib/nhost'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
const publicRoutes = [
|
||||
'/auth/sign-in',
|
||||
'/auth/sign-up',
|
||||
...
|
||||
]
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, route }) {
|
||||
const nhost = await getNhost(cookies)
|
||||
const session = nhost.auth.getSession()
|
||||
|
||||
if (!publicRoutes.includes(route.id ?? '') && !session) {
|
||||
throw redirect(303, '/auth/sign-in')
|
||||
}
|
||||
|
||||
return {
|
||||
user: session?.user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/nhost
|
||||
cd nhost
|
||||
```
|
||||
|
||||
2. Install and build dependencies
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Terminal 1: Start the Nhost Backend
|
||||
|
||||
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
|
||||
|
||||
```sh
|
||||
cd examples/quickstarts/sveltekit
|
||||
nhost up
|
||||
```
|
||||
|
||||
4. Terminal 2: Start the SvelteKit dev server
|
||||
|
||||
```sh
|
||||
cd examples/quickstarts/sveltekit
|
||||
pnpm dev
|
||||
```
|
||||
17
examples/quickstarts/sveltekit/jsconfig.json
Normal file
17
examples/quickstarts/sveltekit/jsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/sveltekit",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -15,10 +15,11 @@
|
||||
"postinstall": "pnpm add-nhost-js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/nhost-js": "2.2.13",
|
||||
"@nhost/nhost-js": "2.2.18",
|
||||
"@playwright/test": "^1.31.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -30,7 +31,7 @@
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.3.0",
|
||||
"vite": "^4.3.8",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -40,6 +41,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"playwright": "^1.37.1",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.0",
|
||||
"xstate": "^4.38.2"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user