Compare commits
145 Commits
@nhost/das
...
@nhost/apo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac3f12c878 | ||
|
|
65cabb089f | ||
|
|
2905beb0a1 | ||
|
|
83fee54460 | ||
|
|
82898b6dae | ||
|
|
500f76a38d | ||
|
|
5e1e80aa8b | ||
|
|
6d0a126907 | ||
|
|
1b7dcf2121 | ||
|
|
2b9205b6cf | ||
|
|
bdc4d4a88c | ||
|
|
45759c4d4c | ||
|
|
5f9886577a | ||
|
|
fa65496327 | ||
|
|
03777680c1 | ||
|
|
72c81207ff | ||
|
|
5ca2a394e8 | ||
|
|
e63b8da58a | ||
|
|
bf8543cd34 | ||
|
|
8a557bbd02 | ||
|
|
327e30b859 | ||
|
|
bbfaf9732b | ||
|
|
c064a53256 | ||
|
|
ebda86f1f0 | ||
|
|
8948be9d3d | ||
|
|
54e9b141f1 | ||
|
|
dba71483df | ||
|
|
77ef68232a | ||
|
|
8fbc7f9f95 | ||
|
|
ca9f0f6ae9 | ||
|
|
e819903f1b | ||
|
|
f780b17581 | ||
|
|
032c0bd217 | ||
|
|
5d278709cb | ||
|
|
3a012e089a | ||
|
|
7aed620e12 | ||
|
|
d9fd1a54a5 | ||
|
|
a19b85c8ac | ||
|
|
4e1aaca0ee | ||
|
|
34ef37cdce | ||
|
|
5d6b655cb1 | ||
|
|
074a0fa111 | ||
|
|
403d839fca | ||
|
|
4e3098240b | ||
|
|
dd0a5cf3c1 | ||
|
|
5187fd3a4b | ||
|
|
d8dfd6bf80 | ||
|
|
6ea6ad61db | ||
|
|
fd0b904ed4 | ||
|
|
8989e314a6 | ||
|
|
5b5a1219c5 | ||
|
|
07fda9bbb3 | ||
|
|
2fa828fef1 | ||
|
|
d5ec69ac37 | ||
|
|
4a7ede11e9 | ||
|
|
482ae4c4f1 | ||
|
|
08fe4cd65f | ||
|
|
5781721bca | ||
|
|
39de0063bf | ||
|
|
202b647234 | ||
|
|
51c163a268 | ||
|
|
6e802c9938 | ||
|
|
9a46104e37 | ||
|
|
655b317c39 | ||
|
|
d3ad7c9d4a | ||
|
|
09fc852c3a | ||
|
|
ece08d3efd | ||
|
|
3493442c2d | ||
|
|
632a79b9e4 | ||
|
|
4a4d85757a | ||
|
|
88a01004b7 | ||
|
|
73230eb35a | ||
|
|
27e1c90624 | ||
|
|
1cc53d550a | ||
|
|
22d3f71e02 | ||
|
|
010b816866 | ||
|
|
4a6e62e673 | ||
|
|
5cf9dd9bc2 | ||
|
|
27e74c10d7 | ||
|
|
bd807a5ee1 | ||
|
|
4093e03a13 | ||
|
|
29076d0304 | ||
|
|
ab83fa6b5e | ||
|
|
b20761e976 | ||
|
|
a445e5b786 | ||
|
|
90df6d81d8 | ||
|
|
aa85084675 | ||
|
|
07ad470c0c | ||
|
|
fa6b58a9c5 | ||
|
|
acf55376ba | ||
|
|
b0a9798b04 | ||
|
|
3952e87f01 | ||
|
|
b95ccf873d | ||
|
|
8d7f84b8da | ||
|
|
bd1b69bd75 | ||
|
|
84d5436634 | ||
|
|
2325766c1d | ||
|
|
2c355eaae4 | ||
|
|
9e26ed767e | ||
|
|
abdb6c56f4 | ||
|
|
3b75bfce27 | ||
|
|
f498190758 | ||
|
|
b4158fa513 | ||
|
|
3d1a177632 | ||
|
|
0675a213b5 | ||
|
|
a8ff383490 | ||
|
|
960d815f68 | ||
|
|
edf2b4e93f | ||
|
|
fe240542a4 | ||
|
|
c7752c0657 | ||
|
|
d1e2b1c75a | ||
|
|
bcdab66bf8 | ||
|
|
7636f40030 | ||
|
|
e643bd3620 | ||
|
|
311c7756d7 | ||
|
|
f967a2e596 | ||
|
|
4c4b253a71 | ||
|
|
0f5f8c0d90 | ||
|
|
37a7fc05d5 | ||
|
|
bf93d87b36 | ||
|
|
efb3dc7294 | ||
|
|
42bd7807b2 | ||
|
|
eea59bd202 | ||
|
|
7248eb733f | ||
|
|
fceb6a4a89 | ||
|
|
b10eca09a8 | ||
|
|
4799b65e96 | ||
|
|
067eb9d6a9 | ||
|
|
219d5ecdcf | ||
|
|
9073182d51 | ||
|
|
bdb5783e79 | ||
|
|
ece717d6e0 | ||
|
|
b135ef695c | ||
|
|
82b3353110 | ||
|
|
3f165a85e3 | ||
|
|
aa4018909f | ||
|
|
98397e3ccd | ||
|
|
911e7112c9 | ||
|
|
e62402ecfc | ||
|
|
9190dd726d | ||
|
|
ae093283d0 | ||
|
|
875327fbea | ||
|
|
3d5c34f4ce | ||
|
|
58c2a20532 | ||
|
|
6c90cb5024 |
@@ -6,5 +6,5 @@
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
"ignore": ["@nhost-examples/sveltekit"]
|
||||
}
|
||||
|
||||
2
.github/workflows/changesets.yaml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v4
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 90
|
||||
with:
|
||||
context: .
|
||||
file: ./dashboard/Dockerfile
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
@@ -146,7 +146,7 @@ jobs:
|
||||
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e tests
|
||||
timeout-minutes: 7
|
||||
timeout-minutes: 15
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
- id: file-name
|
||||
if: ${{ failure() }}
|
||||
|
||||
3
.gitignore
vendored
@@ -19,10 +19,8 @@ logs/
|
||||
coverage/
|
||||
dist/
|
||||
umd/
|
||||
lib/
|
||||
node_modules/
|
||||
tmp/
|
||||
.docz/
|
||||
.pnpm-store
|
||||
.turbo
|
||||
.env
|
||||
@@ -32,7 +30,6 @@ out/
|
||||
# Custom
|
||||
*.min.js
|
||||
*.map
|
||||
todo.md
|
||||
|
||||
# Config files that are not part of the repository root anymore. Should be removed in the future.
|
||||
/.eslintignore
|
||||
|
||||
@@ -1,5 +1,76 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.20.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5e1e80aa8: fix(dashboard): show correct locales in user details
|
||||
- @nhost/react-apollo@5.0.35
|
||||
- @nhost/nextjs@1.13.37
|
||||
|
||||
## 0.20.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.34
|
||||
- @nhost/nextjs@1.13.36
|
||||
|
||||
## 0.20.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4a7ede11e: fix: distinguish files that were not uploaded
|
||||
- 202b64723: feat(nhost-run): add support for one-click-install run services
|
||||
- 074a0fa11: feat(dashboard): add settings toggle to enable/disable antivirus
|
||||
- @nhost/react-apollo@5.0.33
|
||||
- @nhost/nextjs@1.13.35
|
||||
|
||||
## 0.20.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b20761e97: feat(services): add pricing info and confirmation dialog
|
||||
- 90df6d81d: fix(services): handle null values when editing a service
|
||||
- aa8508467: fix: query service logs correctly
|
||||
feat: enable multiline support for environment value input
|
||||
|
||||
## 0.20.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8d7f84b8d: fix: make announcement adapt to theme
|
||||
|
||||
## 0.20.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3b75bfce2: fix: make announcement close properly
|
||||
- f49819075: fix: show correct values when dedicated resources are disabled
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e643bd362: fix(services): fix errors when config is null
|
||||
- bcdab66bf: feat: add annoucement for nhost run
|
||||
- f967a2e59: added note about storage not being able to be downsized
|
||||
- 311c7756d: chore(services): consistent naming for compute
|
||||
|
||||
## 0.20.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9073182d5: chore(dashboard): bump `turbo` to 1.10.11
|
||||
- ece717d6e: feat(logs): show services in the logs page
|
||||
- 82b335311: feat(metrics): change grafana link to point to the dashboards
|
||||
- b135ef695: fix(services): set command as optional and set min replicas to 0
|
||||
|
||||
## 0.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d5c34f4c: fix(auth): fix users pagination limit
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo@1.10.7
|
||||
RUN yarn global add turbo@1.10.11
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.20.0",
|
||||
"version": "0.20.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -66,6 +66,7 @@
|
||||
"react-error-boundary": "^4.0.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
@@ -105,6 +106,7 @@
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/ace": "^0.0.48",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
@@ -0,0 +1,66 @@
|
||||
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);
|
||||
@@ -0,0 +1,92 @@
|
||||
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: 'nhost-run',
|
||||
href: 'https://discord.com/invite/9V7Qb2U',
|
||||
content:
|
||||
'Now you can bring custom and third-party OSS services to run alongside your Nhost projects',
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
3
dashboard/src/components/common/Announcement/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Announcement';
|
||||
export * from './AnnouncementProvider';
|
||||
export { default as useAnnouncement } from './useAnnouncement';
|
||||
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
@@ -178,6 +178,22 @@ export default function DataGridBody<T extends object>({
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundCellColor = (
|
||||
row: Row<T>,
|
||||
column: DataBrowserGridColumn<T>,
|
||||
) => {
|
||||
// Grey out files not uploaded
|
||||
if (!row.values.isUploaded) {
|
||||
return 'grey.200';
|
||||
}
|
||||
|
||||
if (column.isDisabled) {
|
||||
return 'grey.100';
|
||||
}
|
||||
|
||||
return 'background.paper';
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
@@ -260,9 +276,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
cell={cell}
|
||||
sx={{
|
||||
backgroundColor: column.isDisabled
|
||||
? 'grey.100'
|
||||
: 'background.paper',
|
||||
backgroundColor: getBackgroundCellColor(row, column),
|
||||
color: isCellDisabled ? 'text.secondary' : 'text.primary',
|
||||
}}
|
||||
className={twMerge(
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
{...props}
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-3">
|
||||
<div className="grid grid-flow-col items-center gap-3 ">
|
||||
<NavLink href="/" className="w-12">
|
||||
<Logo className="mx-auto cursor-pointer" />
|
||||
</NavLink>
|
||||
|
||||
@@ -38,6 +38,10 @@ query GetAuthenticationSettings($appId: uuid!) {
|
||||
default
|
||||
rating
|
||||
}
|
||||
locale {
|
||||
allowed
|
||||
default
|
||||
}
|
||||
}
|
||||
version
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { copy } from '@/utils/copy';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import {
|
||||
RemoteAppGetUsersDocument,
|
||||
useGetProjectLocalesQuery,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateRemoteAppUserMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -146,6 +147,14 @@ export default function EditUserForm({
|
||||
dataRoles?.config?.auth?.user?.roles?.allowed,
|
||||
);
|
||||
|
||||
const { data } = useGetProjectLocalesQuery({
|
||||
variables: {
|
||||
appId: currentProject?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const allowedLocales = data?.config?.auth?.user?.locale?.allowed || [];
|
||||
|
||||
/**
|
||||
* This will change the `disabled` field in the user to its opposite.
|
||||
* If the user is disabled, it will be enabled and vice versa.
|
||||
@@ -374,12 +383,11 @@ export default function EditUserForm({
|
||||
error={!!errors.locale}
|
||||
helperText={errors?.locale?.message}
|
||||
>
|
||||
<Option key="en" value="en">
|
||||
en
|
||||
</Option>
|
||||
<Option key="fr" value="fr">
|
||||
fr
|
||||
</Option>
|
||||
{allowedLocales.map((locale) => (
|
||||
<Option key={locale} value={locale}>
|
||||
{locale}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
@@ -2,8 +2,9 @@ import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariab
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import '@testing-library/jest-dom';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
import { afterAll, afterEach, beforeAll, test, vi } from 'vitest';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
const server = setupServer(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useHostName } from './useHostName';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function useHostName() {
|
||||
const [hostName, setHostName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const { port, hostname, protocol } = window.location;
|
||||
setHostName(`${protocol}//${hostname}:${port}`);
|
||||
}, []);
|
||||
|
||||
return hostName;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ test('should generate a per service subdomain in remote mode', () => {
|
||||
);
|
||||
|
||||
expect(generateAppServiceUrl('test', region, 'grafana')).toBe(
|
||||
'https://test.grafana.eu-west-1.nhost.run',
|
||||
'https://test.grafana.eu-west-1.nhost.run/dashboards',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ test('should generate staging subdomains in staging environment', () => {
|
||||
);
|
||||
|
||||
expect(generateAppServiceUrl('test', stagingRegion, 'grafana')).toBe(
|
||||
'https://test.grafana.eu-west-1.staging.nhost.run',
|
||||
'https://test.grafana.eu-west-1.staging.nhost.run/dashboards',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ test('should generate no slug for Hasura and Grafana neither in local mode nor i
|
||||
'https://test.hasura.eu-west-1.staging.nhost.run',
|
||||
);
|
||||
expect(generateAppServiceUrl('test', stagingRegion, 'grafana')).toBe(
|
||||
'https://test.grafana.eu-west-1.staging.nhost.run',
|
||||
'https://test.grafana.eu-west-1.staging.nhost.run/dashboards',
|
||||
);
|
||||
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
@@ -129,7 +129,7 @@ test('should generate no slug for Hasura and Grafana neither in local mode nor i
|
||||
'https://test.hasura.eu-west-1.nhost.run',
|
||||
);
|
||||
expect(generateAppServiceUrl('test', region, 'grafana')).toBe(
|
||||
'https://test.grafana.eu-west-1.nhost.run',
|
||||
'https://test.grafana.eu-west-1.nhost.run/dashboards',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -102,5 +102,11 @@ export default function generateAppServiceUrl(
|
||||
.filter(Boolean)
|
||||
.join('.');
|
||||
|
||||
return `https://${constructedDomain}${remoteBackendSlugs[service]}`;
|
||||
let url = `https://${constructedDomain}${remoteBackendSlugs[service]}`;
|
||||
|
||||
if (service === 'grafana') {
|
||||
url = `${url}/dashboards`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { LogsCustomInterval } from '@/features/projects/logs/utils/constant
|
||||
import { LOGS_AVAILABLE_INTERVALS } from '@/features/projects/logs/utils/constants/intervals';
|
||||
import type { AvailableLogsService } from '@/features/projects/logs/utils/constants/services';
|
||||
import { LOGS_AVAILABLE_SERVICES } from '@/features/projects/logs/utils/constants/services';
|
||||
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -132,6 +133,37 @@ export default function LogsHeader({
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const applicationCreationDate = new Date(currentProject.createdAt);
|
||||
|
||||
const [runServices, setRunServices] = useState<
|
||||
{
|
||||
label: string;
|
||||
value: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
const { data, loading } = useGetRunServicesQuery({
|
||||
variables: {
|
||||
appID: currentProject.id,
|
||||
resolve: false,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
const services = data.app?.runServices ?? [];
|
||||
|
||||
setRunServices(
|
||||
services
|
||||
.filter((s) => !!s.config?.name)
|
||||
.map((s) => ({
|
||||
label: s.config.name,
|
||||
value: `run-${s.config.name}`,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}, [loading, data]);
|
||||
|
||||
/**
|
||||
* Will subtract the `customInterval` time in minutes from the current date.
|
||||
*/
|
||||
@@ -181,15 +213,17 @@ export default function LogsHeader({
|
||||
root: { className: 'min-h-[initial] h-9 leading-[initial]' },
|
||||
}}
|
||||
>
|
||||
{LOGS_AVAILABLE_SERVICES.map(({ value, label }) => (
|
||||
<Option
|
||||
key={value}
|
||||
value={value}
|
||||
className="text-sm+ font-medium"
|
||||
>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
{[...LOGS_AVAILABLE_SERVICES, ...runServices].map(
|
||||
({ value, label }) => (
|
||||
<Option
|
||||
key={value}
|
||||
value={value}
|
||||
className="text-sm+ font-medium"
|
||||
>
|
||||
{label}
|
||||
</Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PermissionVariable } from '@/types/application';
|
||||
import { expect, test } from 'vitest';
|
||||
import getAllPermissionVariables from './getAllPermissionVariables';
|
||||
|
||||
test('should convert permission variable object to array', () => {
|
||||
|
||||
@@ -68,11 +68,15 @@ export default function ResourcesConfirmationDialog({
|
||||
const totalBillableVCPU = formValues.enabled ? billableResources.vcpu : 0;
|
||||
const totalBillableMemory = formValues.enabled ? billableResources.memory : 0;
|
||||
|
||||
const updatedPrice =
|
||||
Math.max(
|
||||
priceForTotalAvailableVCPU,
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
|
||||
) + proPlan.price;
|
||||
const { enabled } = formValues;
|
||||
|
||||
const updatedPrice = enabled
|
||||
? Math.max(
|
||||
priceForTotalAvailableVCPU,
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) *
|
||||
RESOURCE_VCPU_PRICE,
|
||||
) + proPlan.price
|
||||
: proPlan.price;
|
||||
|
||||
if (!loading && !proPlan) {
|
||||
return (
|
||||
@@ -86,18 +90,30 @@ export default function ResourcesConfirmationDialog({
|
||||
throw error;
|
||||
}
|
||||
|
||||
const databaseVCPU = enabled ? formValues.database.vcpu : 0;
|
||||
const databaseMemory = enabled ? formValues.database.memory : 0;
|
||||
|
||||
const hasuraVCPU = enabled ? formValues.hasura.vcpu : 0;
|
||||
const hasuraMemory = enabled ? formValues.hasura.memory : 0;
|
||||
|
||||
const authVCPU = enabled ? formValues.auth.vcpu : 0;
|
||||
const authMemory = enabled ? formValues.auth.memory : 0;
|
||||
|
||||
const storageVCPU = enabled ? formValues.storage.vcpu : 0;
|
||||
const storageMemory = enabled ? formValues.storage.memory : 0;
|
||||
|
||||
const databaseResources = `${prettifyVCPU(
|
||||
formValues.database.vcpu,
|
||||
)} vCPU + ${prettifyMemory(formValues.database.memory)}`;
|
||||
const hasuraResources = `${prettifyVCPU(
|
||||
formValues.hasura.vcpu,
|
||||
)} vCPU + ${prettifyMemory(formValues.hasura.memory)}`;
|
||||
const authResources = `${prettifyVCPU(
|
||||
formValues.auth.vcpu,
|
||||
)} vCPU + ${prettifyMemory(formValues.auth.memory)}`;
|
||||
databaseVCPU,
|
||||
)} vCPU + ${prettifyMemory(databaseMemory)}`;
|
||||
const hasuraResources = `${prettifyVCPU(hasuraVCPU)} vCPU + ${prettifyMemory(
|
||||
hasuraMemory,
|
||||
)}`;
|
||||
const authResources = `${prettifyVCPU(authVCPU)} vCPU + ${prettifyMemory(
|
||||
authMemory,
|
||||
)}`;
|
||||
const storageResources = `${prettifyVCPU(
|
||||
formValues.storage.vcpu,
|
||||
)} vCPU + ${prettifyMemory(formValues.storage.memory)}`;
|
||||
storageVCPU,
|
||||
)} vCPU + ${prettifyMemory(storageMemory)}`;
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test } from 'vitest';
|
||||
import { expect, test } from 'vitest';
|
||||
import getAllocatedResources from './getAllocatedResources';
|
||||
|
||||
test('should return the total number of allocated resources', () => {
|
||||
|
||||
@@ -155,3 +155,4 @@ export const MIN_SERVICES_CPU = Math.floor(128 / MEM_CPU_RATIO);
|
||||
export const MIN_SERVICES_MEM = 128;
|
||||
export const MAX_SERVICES_CPU = 7000;
|
||||
export const MAX_SERVICES_MEM = Math.floor(MAX_SERVICES_CPU * MEM_CPU_RATIO);
|
||||
export const COST_PER_VCPU = 0.05;
|
||||
|
||||
@@ -3,16 +3,20 @@ 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 { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useHostName } from '@/features/projects/common/hooks/useHostName';
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import {
|
||||
MAX_SERVICE_REPLICAS,
|
||||
COST_PER_VCPU,
|
||||
MAX_SERVICES_CPU,
|
||||
MAX_SERVICES_MEM,
|
||||
MAX_SERVICE_REPLICAS,
|
||||
MIN_SERVICES_CPU,
|
||||
MIN_SERVICES_MEM,
|
||||
} from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
|
||||
@@ -22,13 +26,15 @@ import { PortsFormSection } from '@/features/services/components/ServiceForm/com
|
||||
import { ReplicasFormSection } from '@/features/services/components/ServiceForm/components/ReplicasFormSection';
|
||||
import { StorageFormSection } from '@/features/services/components/ServiceForm/components/StorageFormSection';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useInsertRunServiceMutation,
|
||||
useReplaceRunServiceConfigMutation,
|
||||
type ConfigRunServiceConfigInsertInput,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -36,6 +42,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { parse } from 'shell-quote';
|
||||
import * as Yup from 'yup';
|
||||
import { ServiceConfirmationDialog } from './components/ServiceConfirmationDialog';
|
||||
|
||||
export enum PortTypes {
|
||||
HTTP = 'http',
|
||||
@@ -46,7 +53,7 @@ export enum PortTypes {
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required.'),
|
||||
image: Yup.string().label('Image to run'),
|
||||
command: Yup.string().required(),
|
||||
command: Yup.string(),
|
||||
environment: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
@@ -57,7 +64,7 @@ export const validationSchema = Yup.object({
|
||||
cpu: Yup.number().min(MIN_SERVICES_CPU).max(MAX_SERVICES_CPU).required(),
|
||||
memory: Yup.number().min(MIN_SERVICES_MEM).max(MAX_SERVICES_MEM).required(),
|
||||
}),
|
||||
replicas: Yup.number().min(1).max(MAX_SERVICE_REPLICAS).required(),
|
||||
replicas: Yup.number().min(0).max(MAX_SERVICE_REPLICAS).required(),
|
||||
ports: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
port: Yup.number().required(),
|
||||
@@ -106,7 +113,8 @@ export default function ServiceForm({
|
||||
onCancel,
|
||||
location,
|
||||
}: ServiceFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const hostName = useHostName();
|
||||
const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
|
||||
const [insertRunService] = useInsertRunServiceMutation();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [insertRunServiceConfig] = useInsertRunServiceConfigMutation();
|
||||
@@ -133,6 +141,8 @@ export default function ServiceForm({
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
} = form;
|
||||
|
||||
const formValues = watch();
|
||||
|
||||
const serviceImage = watch('image');
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
@@ -141,7 +151,7 @@ export default function ServiceForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const getFormattedConfig = (values: ServiceFormValues) => {
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: values.name,
|
||||
image: {
|
||||
@@ -171,7 +181,13 @@ export default function ServiceForm({
|
||||
})),
|
||||
};
|
||||
|
||||
if (initialData) {
|
||||
return config;
|
||||
};
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const config = getFormattedConfig(values);
|
||||
|
||||
if (serviceID) {
|
||||
// Update service config
|
||||
await replaceRunServiceConfig({
|
||||
variables: {
|
||||
@@ -246,10 +262,47 @@ export default function ServiceForm({
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = (values: ServiceFormValues) => {
|
||||
openDialog({
|
||||
title: 'Confirm Resources',
|
||||
component: (
|
||||
<ServiceConfirmationDialog
|
||||
formValues={values}
|
||||
onCancel={closeDialog}
|
||||
onSubmit={async () => {
|
||||
await handleSubmit(formValues);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const pricingExplanation = () => {
|
||||
const vCPUs = `${formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER} vCPUs`;
|
||||
const mem = `${formValues.compute.memory} MiB Mem`;
|
||||
let details = `${vCPUs} + ${mem}`;
|
||||
|
||||
if (formValues.replicas > 1) {
|
||||
details = `(${details}) x ${formValues.replicas} replicas`;
|
||||
}
|
||||
|
||||
return `Approximate cost for ${details}`;
|
||||
};
|
||||
|
||||
const copyConfig = () => {
|
||||
const config = getFormattedConfig(formValues);
|
||||
|
||||
const base64Config = btoa(JSON.stringify(config));
|
||||
|
||||
const link = `${hostName}/run-one-click-install?config=${base64Config}`;
|
||||
|
||||
copy(link, 'Service Config');
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
onSubmit={handleConfirm}
|
||||
className="grid grid-flow-row gap-4 px-6 pb-6"
|
||||
>
|
||||
<Input
|
||||
@@ -258,10 +311,10 @@ export default function ServiceForm({
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Name</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<Tooltip title="Name of the service, must be unique per project.">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="w-4 h-4"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -282,10 +335,26 @@ export default function ServiceForm({
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Image</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Image to use, it can be hosted on any public registry or it
|
||||
can use the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/run/registry"
|
||||
className="underline"
|
||||
>
|
||||
Nhost registry
|
||||
</a>
|
||||
. Image needs to support arm.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="w-4 h-4"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -313,10 +382,10 @@ export default function ServiceForm({
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Command</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<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="w-4 h-4"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -330,6 +399,23 @@ export default function ServiceForm({
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Alert
|
||||
severity="info"
|
||||
className="flex items-center justify-between space-x-2"
|
||||
>
|
||||
<span>{pricingExplanation()}</span>
|
||||
<b>
|
||||
$
|
||||
{parseFloat(
|
||||
(
|
||||
formValues.compute.cpu *
|
||||
formValues.replicas *
|
||||
COST_PER_VCPU
|
||||
).toFixed(2),
|
||||
)}
|
||||
</b>
|
||||
</Alert>
|
||||
|
||||
<ComputeFormSection />
|
||||
|
||||
<ReplicasFormSection />
|
||||
@@ -343,7 +429,7 @@ export default function ServiceForm({
|
||||
{createServiceFormError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
className="grid items-center justify-between grid-flow-col px-4 py-3"
|
||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
||||
>
|
||||
<span className="text-left">
|
||||
<strong>Error:</strong> {createServiceFormError.message}
|
||||
@@ -362,9 +448,24 @@ export default function ServiceForm({
|
||||
</Alert>
|
||||
)}
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{initialData ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
startIcon={<PlusIcon />}
|
||||
>
|
||||
{serviceID ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
disabled={isSubmitting}
|
||||
onClick={copyConfig}
|
||||
startIcon={<CopyIcon />}
|
||||
>
|
||||
Copy one-click install link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
|
||||
@@ -45,14 +45,30 @@ export default function ComputeFormSection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="p-4 space-y-4 rounded border-1">
|
||||
<Box className="space-y-4 rounded border-1 p-4">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
CPU: {formValues.compute.cpu} / Memory: {formValues.compute.memory}
|
||||
vCPUs: {formValues.compute.cpu / 1000} / Memory:{' '}
|
||||
{formValues.compute.memory}
|
||||
</Text>
|
||||
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Compute resources dedicated for the service. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/run/resources"
|
||||
className="underline"
|
||||
>
|
||||
resources
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -62,7 +78,7 @@ export default function ComputeFormSection() {
|
||||
variant="outlined"
|
||||
onClick={decrementCompute}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Slider
|
||||
@@ -79,7 +95,7 @@ export default function ComputeFormSection() {
|
||||
variant="outlined"
|
||||
onClick={incrementCompute}
|
||||
>
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function EnvironmentFormSection() {
|
||||
@@ -15,67 +16,78 @@ export default function EnvironmentFormSection() {
|
||||
formState: { errors },
|
||||
} = useFormContext<ServiceFormValues>();
|
||||
|
||||
const [focusedInput, setFocusedInput] = useState<string>(null);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: 'environment',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="p-4 space-y-4 rounded border-1">
|
||||
<Box className="space-y-4 rounded border-1 p-4">
|
||||
<Box className="flex flex-row items-center justify-between ">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Environment
|
||||
</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Environment variables to add to the service. Other than the ones
|
||||
specified here only <code>NHOST_SUBDOMAIN</code> and{' '}
|
||||
<code>NHOST_REGION</code> are added automatically to the
|
||||
service.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => append({ name: '', value: '' })}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5" />
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box className="flex flex-col space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<Box
|
||||
key={field.id}
|
||||
className="flex w-full flex-col space-y-2 xs+:flex-row xs+:space-y-0 xs+:space-x-2"
|
||||
>
|
||||
<Input
|
||||
{...register(`environment.${index}.name`)}
|
||||
id={`${field.id}-name`}
|
||||
label={!index && 'Name'}
|
||||
placeholder={`Key ${index}`}
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.environment?.at(index)}
|
||||
helperText={errors?.environment?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`environment.${index}.value`)}
|
||||
id={`${field.id}-value`}
|
||||
label={!index && 'Value'}
|
||||
placeholder={`Value ${index}`}
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.environment?.at(index)}
|
||||
helperText={errors?.environment?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Box key={field.id} className="flex w-full items-center space-x-2">
|
||||
<div className="flex w-full flex-col space-y-2">
|
||||
<Input
|
||||
{...register(`environment.${index}.name`)}
|
||||
id={`${field.id}-name`}
|
||||
placeholder={`Key ${index}`}
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.environment?.at(index)}
|
||||
helperText={errors?.environment?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`environment.${index}.value`)}
|
||||
id={`${field.id}-value`}
|
||||
placeholder={`Value ${index}`}
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.environment?.at(index)}
|
||||
helperText={errors?.environment?.at(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
maxRows={focusedInput === `${field.id}-value` ? 1000 : 1}
|
||||
onFocusCapture={() => setFocusedInput(`${field.id}-value`)}
|
||||
onBlurCapture={() => setFocusedInput(null)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="borderless"
|
||||
className=""
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function PortsFormSection() {
|
||||
const port = Number(_port) > 0 ? Number(_port) : '[port]';
|
||||
const name = _name && _name.length > 0 ? _name : '[name]';
|
||||
|
||||
return `https://${currentProject.subdomain}-${name}-${port}.svc.${currentProject.region.awsName}.${currentProject.region.domain}`;
|
||||
return `https://${currentProject?.subdomain}-${name}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -55,7 +55,22 @@ export default function PortsFormSection() {
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Ports
|
||||
</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Network ports to configure for the service. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/run/networking"
|
||||
className="underline"
|
||||
>
|
||||
Networking
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -26,7 +26,24 @@ export default function ReplicasFormSection() {
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Replicas ({replicas})
|
||||
</Text>
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Number of replicas for the service. Multiple replicas can process
|
||||
requests/work in parallel. You can set replicas to 0 to pause the
|
||||
service. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/run/resources"
|
||||
className="underline"
|
||||
>
|
||||
resources
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
|
||||
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
|
||||
export interface ServiceConfirmationDialogProps {
|
||||
/**
|
||||
* The updated resources that the user has selected.
|
||||
*/
|
||||
formValues: ServiceFormValues;
|
||||
/**
|
||||
* Function to be called when the user clicks the cancel button.
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* Function to be called when the user clicks the confirm button.
|
||||
*/
|
||||
onSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ServiceConfirmationDialog({
|
||||
formValues,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: ServiceConfirmationDialogProps) {
|
||||
const approximatePriceForService = parseFloat(
|
||||
(formValues.compute.cpu * formValues.replicas * COST_PER_VCPU).toFixed(2),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||
<Box className="grid grid-flow-row gap-4">
|
||||
<Box className="grid grid-flow-row gap-1.5">
|
||||
<Box className="grid grid-flow-col items-center justify-between gap-2">
|
||||
<Box className="grid grid-flow-row gap-0.5">
|
||||
<Text color="secondary">vCPUs</Text>
|
||||
</Box>
|
||||
<Text>{formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER}</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-col items-center justify-between gap-2">
|
||||
<Box className="grid grid-flow-row gap-0.5">
|
||||
<Text color="secondary">Memory</Text>
|
||||
</Box>
|
||||
<Text>{formValues.compute.memory} MiB</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-col items-center justify-between gap-2">
|
||||
<Box className="grid grid-flow-row gap-0.5">
|
||||
<Text color="secondary">Replicas</Text>
|
||||
</Box>
|
||||
<Text>{formValues.replicas}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box className="grid grid-flow-col justify-between gap-2">
|
||||
<Box className="grid grid-flow-col items-center gap-1.5">
|
||||
<Text className="font-medium">Approximate Cost</Text>
|
||||
|
||||
<Tooltip title="$0.0012/minute for every 1 vCPU and 2 GiB of RAM">
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Text>${approximatePriceForService}/mo</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
<Button color="primary" onClick={onSubmit} autoFocus>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
<Button variant="borderless" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ServiceConfirmationDialog';
|
||||
export { default as ServiceConfirmationDialog } from './ServiceConfirmationDialog';
|
||||
@@ -41,15 +41,32 @@ export default function StorageFormSection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="p-4 space-y-4 rounded border-1">
|
||||
<Box className="space-y-4 rounded border-1 p-4">
|
||||
<Box className="flex flex-row items-center justify-between ">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Storage
|
||||
</Text>
|
||||
|
||||
<Tooltip title="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s">
|
||||
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
By default, services do not have persistent storage. You can add
|
||||
SSD disks to the service here. It is important to note that
|
||||
capacity can not be decreased after creation, only expanded. Refer to{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.nhost.io/run/storage"
|
||||
className="underline"
|
||||
>
|
||||
Storage
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
@@ -57,7 +74,7 @@ export default function StorageFormSection() {
|
||||
variant="borderless"
|
||||
onClick={() => append({ name: '', capacity: 1, path: '' })}
|
||||
>
|
||||
<PlusIcon className="w-5 h-5" />
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -120,7 +137,7 @@ export default function StorageFormSection() {
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -63,18 +63,11 @@ export default function ServicesList({
|
||||
};
|
||||
|
||||
const viewService = async (service: RunService) => {
|
||||
const {
|
||||
image,
|
||||
command,
|
||||
ports,
|
||||
resources: { compute, replicas, storage },
|
||||
} = service.config;
|
||||
|
||||
openDrawer({
|
||||
title: (
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<CubeIcon className="h-5 w-5" />
|
||||
<Text>Edit {service.config.name}</Text>
|
||||
<Text>Edit {service.config?.name ?? 'unset'}</Text>
|
||||
</Box>
|
||||
),
|
||||
component: (
|
||||
@@ -82,16 +75,19 @@ export default function ServicesList({
|
||||
serviceID={service.id}
|
||||
initialData={{
|
||||
...service.config,
|
||||
image: image.image,
|
||||
command: command?.join(' '),
|
||||
ports: ports.map((item) => ({
|
||||
image: service.config?.image?.image,
|
||||
command: service.config?.command?.join(' '),
|
||||
ports: service.config?.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type as PortTypes,
|
||||
publish: item.publish,
|
||||
})),
|
||||
compute,
|
||||
replicas,
|
||||
storage,
|
||||
compute: service.config?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: service.config?.resources?.replicas,
|
||||
storage: service.config?.resources?.storage,
|
||||
}}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
@@ -146,7 +142,7 @@ export default function ServicesList({
|
||||
<CubeIcon className="h-5 w-5" />
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
{service.config.name}
|
||||
{service.config?.name ?? 'unset'}
|
||||
</Text>
|
||||
<Tooltip title={service.updatedAt}>
|
||||
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
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 { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetStorageSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraStorageAVFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraStorageAVSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetStorageSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { server } = data?.config?.storage?.antivirus || {};
|
||||
|
||||
const form = useForm<HasuraStorageAVFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: !!server,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading AV settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraStorageAVFormValues) {
|
||||
let antivirus = null;
|
||||
|
||||
if (formValues.enabled) {
|
||||
antivirus = {
|
||||
server: 'tcp://run-clamav:3310',
|
||||
};
|
||||
}
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
storage: {
|
||||
antivirus,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Antivirus settings are being updated...`,
|
||||
success: `Antivirus settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update Antivirus settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Antivirus"
|
||||
description="Enable or disable Antivirus."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Antivirus"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraStorageAVSettings';
|
||||
export { default as HasuraStorageAVSettings } from './HasuraStorageAVSettings';
|
||||
@@ -4,6 +4,9 @@ query GetStorageSettings($appId: uuid!) {
|
||||
__typename
|
||||
storage {
|
||||
version
|
||||
antivirus {
|
||||
server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
dashboard/src/gql/app/getProjectLocales.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
query getProjectLocales($appId: uuid!) {
|
||||
config(appID: $appId, resolve: true) {
|
||||
auth {
|
||||
user {
|
||||
locale {
|
||||
allowed
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,35 @@ import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
|
||||
|
||||
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
||||
import { ServiceForm } from '@/features/services/components/ServiceForm';
|
||||
import {
|
||||
ServiceForm,
|
||||
type PortTypes,
|
||||
} from '@/features/services/components/ServiceForm';
|
||||
import ServicesList from '@/features/services/components/ServicesList/ServicesList';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
|
||||
export type RunService = Omit<
|
||||
GetRunServicesQuery['app']['runServices'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export type RunServiceConfig = Omit<
|
||||
GetRunServicesQuery['app']['runServices'][0]['config'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function ServicesPage() {
|
||||
const limit = useRef(25);
|
||||
const router = useRouter();
|
||||
const { openDrawer } = useDialog();
|
||||
const { openDrawer, openAlertDialog } = useDialog();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const isPlanFree = currentProject.plan.isFree;
|
||||
|
||||
@@ -66,6 +81,63 @@ export default function ServicesPage() {
|
||||
[data],
|
||||
);
|
||||
|
||||
const checkConfigFromQuery = useCallback(
|
||||
(base64Config: string) => {
|
||||
if (router.query?.config) {
|
||||
try {
|
||||
const decodedConfig = atob(base64Config);
|
||||
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
|
||||
|
||||
openDrawer({
|
||||
title: (
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<CubeIcon className="h-5 w-5" />
|
||||
<Text>Create a new run service</Text>
|
||||
</Box>
|
||||
),
|
||||
component: (
|
||||
<ServiceForm
|
||||
initialData={{
|
||||
...parsedConfig,
|
||||
compute: parsedConfig?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
image: parsedConfig?.image?.image,
|
||||
command: parsedConfig?.command?.join(' '),
|
||||
ports: parsedConfig?.ports.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type as PortTypes,
|
||||
publish: item.publish,
|
||||
})),
|
||||
replicas: parsedConfig?.resources?.replicas,
|
||||
storage: parsedConfig?.resources?.storage,
|
||||
}}
|
||||
onSubmit={refetchServices}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
openAlertDialog({
|
||||
title: 'Configuration not set properly',
|
||||
payload: 'The service configuration was not properly encoded',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[router.query.config, openDrawer, refetchServices, openAlertDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query?.config) {
|
||||
checkConfigFromQuery(router.query?.config as string);
|
||||
}
|
||||
}, [checkConfigFromQuery, router.query]);
|
||||
|
||||
const openCreateServiceDialog = () => {
|
||||
openDrawer({
|
||||
title: (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { StorageServiceVersionSettings } from '@/features/storage/settings/components/HasuraServiceVersionSettings';
|
||||
import { HasuraStorageAVSettings } from '@/features/storage/settings/components/HasuraStorageAVSettings';
|
||||
import { useGetStorageSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function StorageSettingsPage() {
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<StorageServiceVersionSettings />
|
||||
<HasuraStorageAVSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function UsersPage() {
|
||||
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
|
||||
const [searchString, setSearchString] = useState<string>('');
|
||||
|
||||
const limit = useRef(1);
|
||||
const limit = useRef(25);
|
||||
const router = useRouter();
|
||||
const [nrOfPages, setNrOfPages] = useState(
|
||||
parseInt(router.query.page as string, 10) || 1,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -105,7 +106,9 @@ function MyApp({
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<DialogProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
<AnnouncementProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</AnnouncementProvider>
|
||||
</DialogProvider>
|
||||
</RetryableErrorBoundary>
|
||||
</ThemeProvider>
|
||||
|
||||
218
dashboard/src/pages/run-one-click-install.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
|
||||
import {
|
||||
useGetAllWorkspacesAndProjectsQuery,
|
||||
type GetAllWorkspacesAndProjectsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent, ReactElement } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type Workspace = Omit<
|
||||
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function SelectWorkspaceAndProject() {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
const { openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
|
||||
skip: !user,
|
||||
});
|
||||
|
||||
const workspaces: Workspace[] = data?.workspaces || [];
|
||||
|
||||
const projects = workspaces.flatMap((workspace) =>
|
||||
workspace.projects.map((project) => ({
|
||||
workspaceName: workspace.name,
|
||||
projectName: project.name,
|
||||
value: `${workspace.slug}/${project.slug}`,
|
||||
isFree: project.plan.isFree,
|
||||
})),
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const checkConfigFromQuery = useCallback(
|
||||
(base64Config: string) => {
|
||||
try {
|
||||
JSON.parse(atob(base64Config));
|
||||
} catch (error) {
|
||||
openAlertDialog({
|
||||
title: 'Configuration not set properly',
|
||||
payload:
|
||||
'Either the link is wrong or the configuration is not properly encoded',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
onPrimaryAction: async () => {
|
||||
await router.push('/');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[openAlertDialog, router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
checkConfigFromQuery(router.query?.config as string);
|
||||
}, [checkConfigFromQuery, router.query]);
|
||||
|
||||
const goToServices = async (project: {
|
||||
workspaceName: string;
|
||||
projectName: string;
|
||||
value: string;
|
||||
isFree: boolean;
|
||||
}) => {
|
||||
if (!project) {
|
||||
openAlertDialog({
|
||||
title: 'Please select a workspace and a project',
|
||||
payload:
|
||||
'You must select a workspace and a project before proceeding to create the run service',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.isFree) {
|
||||
openAlertDialog({
|
||||
title: 'The project must have a pro plan',
|
||||
payload: 'Creating run services is only availabel for pro projects',
|
||||
props: {
|
||||
primaryButtonText: 'Ok',
|
||||
hideSecondaryAction: true,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push({
|
||||
pathname: `/${project.value}/services`,
|
||||
// Keep the same query params that got us here
|
||||
query: router.query,
|
||||
});
|
||||
};
|
||||
|
||||
const projectsToDisplay = filter
|
||||
? projects.filter((project) =>
|
||||
project.projectName.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: projects;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading workspaces and projects..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="mx-auto grid max-w-[760px] grid-flow-row gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
New Run Service
|
||||
</Text>
|
||||
|
||||
<InfoCard
|
||||
title="Please select the workspace and the project where you want to create the service"
|
||||
disableCopy
|
||||
value=""
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{projectsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{projectsToDisplay.map((project, index) => (
|
||||
<Fragment key={project.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToServices(project)}
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={project.projectName}
|
||||
secondary={`${project.workspaceName} / ${project.projectName}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < projects.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="New Run Service">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
397
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -1805,6 +1805,7 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
|
||||
/** Configuration for storage service */
|
||||
export type ConfigStorage = {
|
||||
__typename?: 'ConfigStorage';
|
||||
antivirus?: Maybe<ConfigStorageAntivirus>;
|
||||
/** Resources for the service */
|
||||
resources?: Maybe<ConfigResources>;
|
||||
/**
|
||||
@@ -1818,20 +1819,43 @@ export type ConfigStorage = {
|
||||
version?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirus = {
|
||||
__typename?: 'ConfigStorageAntivirus';
|
||||
server?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigStorageAntivirusComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigStorageAntivirusComparisonExp>>;
|
||||
server?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusInsertInput = {
|
||||
server?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageAntivirusUpdateInput = {
|
||||
server?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigStorageComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigStorageComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigStorageComparisonExp>>;
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
|
||||
resources?: InputMaybe<ConfigResourcesComparisonExp>;
|
||||
version?: InputMaybe<ConfigStringComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigStorageInsertInput = {
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusInsertInput>;
|
||||
resources?: InputMaybe<ConfigResourcesInsertInput>;
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigStorageUpdateInput = {
|
||||
antivirus?: InputMaybe<ConfigStorageAntivirusUpdateInput>;
|
||||
resources?: InputMaybe<ConfigResourcesUpdateInput>;
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@@ -2028,6 +2052,12 @@ export type Int_Comparison_Exp = {
|
||||
_nin?: InputMaybe<Array<Scalars['Int']>>;
|
||||
};
|
||||
|
||||
export type InvoiceSummary = {
|
||||
__typename?: 'InvoiceSummary';
|
||||
AmountDue: Scalars['float64'];
|
||||
PeriodEnd: Scalars['Timestamp'];
|
||||
};
|
||||
|
||||
export type Log = {
|
||||
__typename?: 'Log';
|
||||
log: Scalars['String'];
|
||||
@@ -10451,6 +10481,10 @@ export type Mutation_Root = {
|
||||
deleteUser?: Maybe<Users>;
|
||||
/** delete data from the table: "auth.users" */
|
||||
deleteUsers?: Maybe<Users_Mutation_Response>;
|
||||
/** delete single row from the table: "users_usage" */
|
||||
deleteUsersUsage?: Maybe<Users_Usage>;
|
||||
/** delete data from the table: "users_usage" */
|
||||
deleteUsersUsages?: Maybe<Users_Usage_Mutation_Response>;
|
||||
/** delete single row from the table: "workspaces" */
|
||||
deleteWorkspace?: Maybe<Workspaces>;
|
||||
/** delete single row from the table: "workspace_members" */
|
||||
@@ -10590,6 +10624,10 @@ export type Mutation_Root = {
|
||||
insertUser?: Maybe<Users>;
|
||||
/** insert data into the table: "auth.users" */
|
||||
insertUsers?: Maybe<Users_Mutation_Response>;
|
||||
/** insert a single row into the table: "users_usage" */
|
||||
insertUsersUsage?: Maybe<Users_Usage>;
|
||||
/** insert data into the table: "users_usage" */
|
||||
insertUsersUsages?: Maybe<Users_Usage_Mutation_Response>;
|
||||
/** insert a single row into the table: "workspaces" */
|
||||
insertWorkspace?: Maybe<Workspaces>;
|
||||
/** insert a single row into the table: "workspace_members" */
|
||||
@@ -10736,6 +10774,10 @@ export type Mutation_Root = {
|
||||
updateUser?: Maybe<Users>;
|
||||
/** update data of the table: "auth.users" */
|
||||
updateUsers?: Maybe<Users_Mutation_Response>;
|
||||
/** update single row of the table: "users_usage" */
|
||||
updateUsersUsage?: Maybe<Users_Usage>;
|
||||
/** update data of the table: "users_usage" */
|
||||
updateUsersUsages?: Maybe<Users_Usage_Mutation_Response>;
|
||||
/** update single row of the table: "workspaces" */
|
||||
updateWorkspace?: Maybe<Workspaces>;
|
||||
/** update single row of the table: "workspace_members" */
|
||||
@@ -10830,6 +10872,8 @@ export type Mutation_Root = {
|
||||
update_run_service_many?: Maybe<Array<Maybe<Run_Service_Mutation_Response>>>;
|
||||
/** update multiples rows of table: "auth.users" */
|
||||
update_users_many?: Maybe<Array<Maybe<Users_Mutation_Response>>>;
|
||||
/** update multiples rows of table: "users_usage" */
|
||||
update_users_usage_many?: Maybe<Array<Maybe<Users_Usage_Mutation_Response>>>;
|
||||
/** update multiples rows of table: "workspace_member_invites" */
|
||||
update_workspaceMemberInvites_many?: Maybe<Array<Maybe<WorkspaceMemberInvites_Mutation_Response>>>;
|
||||
/** update multiples rows of table: "workspace_members" */
|
||||
@@ -11260,6 +11304,18 @@ export type Mutation_RootDeleteUsersArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteUsersUsageArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteUsersUsagesArgs = {
|
||||
where: Users_Usage_Bool_Exp;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootDeleteWorkspaceArgs = {
|
||||
id: Scalars['uuid'];
|
||||
@@ -11744,6 +11800,20 @@ export type Mutation_RootInsertUsersArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertUsersUsageArgs = {
|
||||
object: Users_Usage_Insert_Input;
|
||||
on_conflict?: InputMaybe<Users_Usage_On_Conflict>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertUsersUsagesArgs = {
|
||||
objects: Array<Users_Usage_Insert_Input>;
|
||||
on_conflict?: InputMaybe<Users_Usage_On_Conflict>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertWorkspaceArgs = {
|
||||
object: Workspaces_Insert_Input;
|
||||
@@ -12359,6 +12429,20 @@ export type Mutation_RootUpdateUsersArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateUsersUsageArgs = {
|
||||
_set?: InputMaybe<Users_Usage_Set_Input>;
|
||||
pk_columns: Users_Usage_Pk_Columns_Input;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateUsersUsagesArgs = {
|
||||
_set?: InputMaybe<Users_Usage_Set_Input>;
|
||||
where: Users_Usage_Bool_Exp;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdateWorkspaceArgs = {
|
||||
_set?: InputMaybe<Workspaces_Set_Input>;
|
||||
@@ -12661,6 +12745,12 @@ export type Mutation_RootUpdate_Users_ManyArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdate_Users_Usage_ManyArgs = {
|
||||
updates: Array<Users_Usage_Updates>;
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootUpdate_WorkspaceMemberInvites_ManyArgs = {
|
||||
updates: Array<WorkspaceMemberInvites_Updates>;
|
||||
@@ -13881,7 +13971,7 @@ export type Query_Root = {
|
||||
billingDedicatedComputeReportsAggregate: Billing_Dedicated_Compute_Reports_Aggregate;
|
||||
/** fetch data from the table: "billing.dedicated_compute" */
|
||||
billingDedicatedComputes: Array<Billing_Dedicated_Compute>;
|
||||
billingDummy: Scalars['Boolean'];
|
||||
billingGetNextInvoice?: Maybe<InvoiceSummary>;
|
||||
/** fetch data from the table: "billing.subscriptions" using primary key columns */
|
||||
billingSubscription?: Maybe<Billing_Subscriptions>;
|
||||
/** fetch data from the table: "billing.subscriptions" */
|
||||
@@ -14024,6 +14114,12 @@ export type Query_Root = {
|
||||
users: Array<Users>;
|
||||
/** fetch aggregated fields from the table: "auth.users" */
|
||||
usersAggregate: Users_Aggregate;
|
||||
/** fetch data from the table: "users_usage" using primary key columns */
|
||||
usersUsage?: Maybe<Users_Usage>;
|
||||
/** fetch data from the table: "users_usage" */
|
||||
usersUsages: Array<Users_Usage>;
|
||||
/** fetch aggregated fields from the table: "users_usage" */
|
||||
usersUsagesAggregate: Users_Usage_Aggregate;
|
||||
/** fetch data from the table: "workspaces" using primary key columns */
|
||||
workspace?: Maybe<Workspaces>;
|
||||
/** fetch data from the table: "workspace_members" using primary key columns */
|
||||
@@ -14395,6 +14491,11 @@ export type Query_RootBillingDedicatedComputesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootBillingGetNextInvoiceArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootBillingSubscriptionArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
@@ -14949,6 +15050,29 @@ export type Query_RootUsersAggregateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootUsersUsageArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootUsersUsagesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Users_Usage_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Users_Usage_Order_By>>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootUsersUsagesAggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Users_Usage_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Users_Usage_Order_By>>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Query_RootWorkspaceArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
@@ -16377,8 +16501,16 @@ export type Subscription_Root = {
|
||||
users: Array<Users>;
|
||||
/** fetch aggregated fields from the table: "auth.users" */
|
||||
usersAggregate: Users_Aggregate;
|
||||
/** fetch data from the table: "users_usage" using primary key columns */
|
||||
usersUsage?: Maybe<Users_Usage>;
|
||||
/** fetch data from the table: "users_usage" */
|
||||
usersUsages: Array<Users_Usage>;
|
||||
/** fetch aggregated fields from the table: "users_usage" */
|
||||
usersUsagesAggregate: Users_Usage_Aggregate;
|
||||
/** fetch data from the table in a streaming manner: "auth.users" */
|
||||
users_stream: Array<Users>;
|
||||
/** fetch data from the table in a streaming manner: "users_usage" */
|
||||
users_usage_stream: Array<Users_Usage>;
|
||||
/** fetch data from the table: "workspaces" using primary key columns */
|
||||
workspace?: Maybe<Workspaces>;
|
||||
/** fetch data from the table: "workspace_members" using primary key columns */
|
||||
@@ -17426,6 +17558,29 @@ export type Subscription_RootUsersAggregateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootUsersUsageArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootUsersUsagesArgs = {
|
||||
distinct_on?: InputMaybe<Array<Users_Usage_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Users_Usage_Order_By>>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootUsersUsagesAggregateArgs = {
|
||||
distinct_on?: InputMaybe<Array<Users_Usage_Select_Column>>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
offset?: InputMaybe<Scalars['Int']>;
|
||||
order_by?: InputMaybe<Array<Users_Usage_Order_By>>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootUsers_StreamArgs = {
|
||||
batch_size: Scalars['Int'];
|
||||
cursor: Array<InputMaybe<Users_Stream_Cursor_Input>>;
|
||||
@@ -17433,6 +17588,13 @@ export type Subscription_RootUsers_StreamArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootUsers_Usage_StreamArgs = {
|
||||
batch_size: Scalars['Int'];
|
||||
cursor: Array<InputMaybe<Users_Usage_Stream_Cursor_Input>>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
|
||||
export type Subscription_RootWorkspaceArgs = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
@@ -18515,6 +18677,176 @@ export type Users_Updates = {
|
||||
where: Users_Bool_Exp;
|
||||
};
|
||||
|
||||
/** columns and relationships of "users_usage" */
|
||||
export type Users_Usage = {
|
||||
__typename?: 'users_usage';
|
||||
created_at: Scalars['timestamptz'];
|
||||
free_allowance_exceeded: Scalars['Boolean'];
|
||||
id: Scalars['uuid'];
|
||||
updated_at: Scalars['timestamptz'];
|
||||
user_id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
/** aggregated selection of "users_usage" */
|
||||
export type Users_Usage_Aggregate = {
|
||||
__typename?: 'users_usage_aggregate';
|
||||
aggregate?: Maybe<Users_Usage_Aggregate_Fields>;
|
||||
nodes: Array<Users_Usage>;
|
||||
};
|
||||
|
||||
/** aggregate fields of "users_usage" */
|
||||
export type Users_Usage_Aggregate_Fields = {
|
||||
__typename?: 'users_usage_aggregate_fields';
|
||||
count: Scalars['Int'];
|
||||
max?: Maybe<Users_Usage_Max_Fields>;
|
||||
min?: Maybe<Users_Usage_Min_Fields>;
|
||||
};
|
||||
|
||||
|
||||
/** aggregate fields of "users_usage" */
|
||||
export type Users_Usage_Aggregate_FieldsCountArgs = {
|
||||
columns?: InputMaybe<Array<Users_Usage_Select_Column>>;
|
||||
distinct?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
/** Boolean expression to filter rows from the table "users_usage". All fields are combined with a logical 'AND'. */
|
||||
export type Users_Usage_Bool_Exp = {
|
||||
_and?: InputMaybe<Array<Users_Usage_Bool_Exp>>;
|
||||
_not?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
_or?: InputMaybe<Array<Users_Usage_Bool_Exp>>;
|
||||
created_at?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
free_allowance_exceeded?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
updated_at?: InputMaybe<Timestamptz_Comparison_Exp>;
|
||||
user_id?: InputMaybe<Uuid_Comparison_Exp>;
|
||||
};
|
||||
|
||||
/** unique or primary key constraints on table "users_usage" */
|
||||
export enum Users_Usage_Constraint {
|
||||
/** unique or primary key constraint on columns "id" */
|
||||
UsersUsagePkey = 'users_usage_pkey',
|
||||
/** unique or primary key constraint on columns "user_id" */
|
||||
UsersUsageUserIdKey = 'users_usage_user_id_key'
|
||||
}
|
||||
|
||||
/** input type for inserting data into table "users_usage" */
|
||||
export type Users_Usage_Insert_Input = {
|
||||
created_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
free_allowance_exceeded?: InputMaybe<Scalars['Boolean']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
updated_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
user_id?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** aggregate max on columns */
|
||||
export type Users_Usage_Max_Fields = {
|
||||
__typename?: 'users_usage_max_fields';
|
||||
created_at?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
updated_at?: Maybe<Scalars['timestamptz']>;
|
||||
user_id?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** aggregate min on columns */
|
||||
export type Users_Usage_Min_Fields = {
|
||||
__typename?: 'users_usage_min_fields';
|
||||
created_at?: Maybe<Scalars['timestamptz']>;
|
||||
id?: Maybe<Scalars['uuid']>;
|
||||
updated_at?: Maybe<Scalars['timestamptz']>;
|
||||
user_id?: Maybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** response of any mutation on the table "users_usage" */
|
||||
export type Users_Usage_Mutation_Response = {
|
||||
__typename?: 'users_usage_mutation_response';
|
||||
/** number of rows affected by the mutation */
|
||||
affected_rows: Scalars['Int'];
|
||||
/** data from the rows affected by the mutation */
|
||||
returning: Array<Users_Usage>;
|
||||
};
|
||||
|
||||
/** on_conflict condition type for table "users_usage" */
|
||||
export type Users_Usage_On_Conflict = {
|
||||
constraint: Users_Usage_Constraint;
|
||||
update_columns?: Array<Users_Usage_Update_Column>;
|
||||
where?: InputMaybe<Users_Usage_Bool_Exp>;
|
||||
};
|
||||
|
||||
/** Ordering options when selecting data from "users_usage". */
|
||||
export type Users_Usage_Order_By = {
|
||||
created_at?: InputMaybe<Order_By>;
|
||||
free_allowance_exceeded?: InputMaybe<Order_By>;
|
||||
id?: InputMaybe<Order_By>;
|
||||
updated_at?: InputMaybe<Order_By>;
|
||||
user_id?: InputMaybe<Order_By>;
|
||||
};
|
||||
|
||||
/** primary key columns input for table: users_usage */
|
||||
export type Users_Usage_Pk_Columns_Input = {
|
||||
id: Scalars['uuid'];
|
||||
};
|
||||
|
||||
/** select columns of table "users_usage" */
|
||||
export enum Users_Usage_Select_Column {
|
||||
/** column name */
|
||||
CreatedAt = 'created_at',
|
||||
/** column name */
|
||||
FreeAllowanceExceeded = 'free_allowance_exceeded',
|
||||
/** column name */
|
||||
Id = 'id',
|
||||
/** column name */
|
||||
UpdatedAt = 'updated_at',
|
||||
/** column name */
|
||||
UserId = 'user_id'
|
||||
}
|
||||
|
||||
/** input type for updating data in table "users_usage" */
|
||||
export type Users_Usage_Set_Input = {
|
||||
created_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
free_allowance_exceeded?: InputMaybe<Scalars['Boolean']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
updated_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
user_id?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** Streaming cursor of the table "users_usage" */
|
||||
export type Users_Usage_Stream_Cursor_Input = {
|
||||
/** Stream column input with initial value */
|
||||
initial_value: Users_Usage_Stream_Cursor_Value_Input;
|
||||
/** cursor ordering */
|
||||
ordering?: InputMaybe<Cursor_Ordering>;
|
||||
};
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Users_Usage_Stream_Cursor_Value_Input = {
|
||||
created_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
free_allowance_exceeded?: InputMaybe<Scalars['Boolean']>;
|
||||
id?: InputMaybe<Scalars['uuid']>;
|
||||
updated_at?: InputMaybe<Scalars['timestamptz']>;
|
||||
user_id?: InputMaybe<Scalars['uuid']>;
|
||||
};
|
||||
|
||||
/** update columns of table "users_usage" */
|
||||
export enum Users_Usage_Update_Column {
|
||||
/** column name */
|
||||
CreatedAt = 'created_at',
|
||||
/** column name */
|
||||
FreeAllowanceExceeded = 'free_allowance_exceeded',
|
||||
/** column name */
|
||||
Id = 'id',
|
||||
/** column name */
|
||||
UpdatedAt = 'updated_at',
|
||||
/** column name */
|
||||
UserId = 'user_id'
|
||||
}
|
||||
|
||||
export type Users_Usage_Updates = {
|
||||
/** sets the columns of the filtered rows to the given values */
|
||||
_set?: InputMaybe<Users_Usage_Set_Input>;
|
||||
/** filter the rows which have to be updated */
|
||||
where: Users_Usage_Bool_Exp;
|
||||
};
|
||||
|
||||
/** Boolean expression to compare columns of type "uuid". All fields are combined with logical 'AND'. */
|
||||
export type Uuid_Comparison_Exp = {
|
||||
_eq?: InputMaybe<Scalars['uuid']>;
|
||||
@@ -19694,7 +20026,7 @@ export type GetAuthenticationSettingsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null } | null } | null } | null };
|
||||
export type GetAuthenticationSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', auth?: { __typename: 'ConfigAuth', version?: string | null, id: 'ConfigAuth', redirections?: { __typename?: 'ConfigAuthRedirections', clientUrl?: any | null, allowedUrls?: Array<string> | null } | null, totp?: { __typename?: 'ConfigAuthTotp', enabled?: boolean | null, issuer?: string | null } | null, signUp?: { __typename?: 'ConfigAuthSignUp', enabled?: boolean | null } | null, session?: { __typename?: 'ConfigAuthSession', accessToken?: { __typename?: 'ConfigAuthSessionAccessToken', expiresIn?: any | null } | null, refreshToken?: { __typename?: 'ConfigAuthSessionRefreshToken', expiresIn?: any | null } | null } | null, user?: { __typename?: 'ConfigAuthUser', email?: { __typename?: 'ConfigAuthUserEmail', allowed?: Array<any> | null, blocked?: Array<any> | null } | null, emailDomains?: { __typename?: 'ConfigAuthUserEmailDomains', allowed?: Array<string> | null, blocked?: Array<string> | null } | null, gravatar?: { __typename?: 'ConfigAuthUserGravatar', enabled?: boolean | null, default?: string | null, rating?: string | null } | null, locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null } | null } | null };
|
||||
|
||||
export type GetPostgresSettingsQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -19750,7 +20082,7 @@ export type GetStorageSettingsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetStorageSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', storage?: { __typename?: 'ConfigStorage', version?: string | null } | null } | null };
|
||||
export type GetStorageSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', storage?: { __typename?: 'ConfigStorage', version?: string | null, antivirus?: { __typename?: 'ConfigStorageAntivirus', server?: string | null } | null } | null } | null };
|
||||
|
||||
export type DeleteApplicationMutationVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
@@ -19791,6 +20123,13 @@ export type GetApplicationStateQueryVariables = Exact<{
|
||||
|
||||
export type GetApplicationStateQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', id: any, name: string, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }> } | null };
|
||||
|
||||
export type GetProjectLocalesQueryVariables = Exact<{
|
||||
appId: Scalars['uuid'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectLocalesQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', auth?: { __typename?: 'ConfigAuth', user?: { __typename?: 'ConfigAuthUser', locale?: { __typename?: 'ConfigAuthUserLocale', allowed?: Array<any> | null, default?: any | null } | null } | null } | null } | null };
|
||||
|
||||
export type GetProjectMetricsQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
subdomain: Scalars['String'];
|
||||
@@ -20761,6 +21100,10 @@ export const GetAuthenticationSettingsDocument = gql`
|
||||
default
|
||||
rating
|
||||
}
|
||||
locale {
|
||||
allowed
|
||||
default
|
||||
}
|
||||
}
|
||||
version
|
||||
}
|
||||
@@ -21063,6 +21406,9 @@ export const GetStorageSettingsDocument = gql`
|
||||
__typename
|
||||
storage {
|
||||
version
|
||||
antivirus {
|
||||
server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21299,6 +21645,51 @@ export type GetApplicationStateQueryResult = Apollo.QueryResult<GetApplicationSt
|
||||
export function refetchGetApplicationStateQuery(variables: GetApplicationStateQueryVariables) {
|
||||
return { query: GetApplicationStateDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectLocalesDocument = gql`
|
||||
query getProjectLocales($appId: uuid!) {
|
||||
config(appID: $appId, resolve: true) {
|
||||
auth {
|
||||
user {
|
||||
locale {
|
||||
allowed
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetProjectLocalesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetProjectLocalesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetProjectLocalesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetProjectLocalesQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetProjectLocalesQuery(baseOptions: Apollo.QueryHookOptions<GetProjectLocalesQuery, GetProjectLocalesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetProjectLocalesQuery, GetProjectLocalesQueryVariables>(GetProjectLocalesDocument, options);
|
||||
}
|
||||
export function useGetProjectLocalesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectLocalesQuery, GetProjectLocalesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetProjectLocalesQuery, GetProjectLocalesQueryVariables>(GetProjectLocalesDocument, options);
|
||||
}
|
||||
export type GetProjectLocalesQueryHookResult = ReturnType<typeof useGetProjectLocalesQuery>;
|
||||
export type GetProjectLocalesLazyQueryHookResult = ReturnType<typeof useGetProjectLocalesLazyQuery>;
|
||||
export type GetProjectLocalesQueryResult = Apollo.QueryResult<GetProjectLocalesQuery, GetProjectLocalesQueryVariables>;
|
||||
export function refetchGetProjectLocalesQuery(variables: GetProjectLocalesQueryVariables) {
|
||||
return { query: GetProjectLocalesDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectMetricsDocument = gql`
|
||||
query GetProjectMetrics($appId: String!, $subdomain: String!, $from: Timestamp, $to: Timestamp) {
|
||||
logsVolume: getLogsVolume(appID: $appId, from: $from, to: $to) {
|
||||
|
||||
@@ -18,6 +18,8 @@ module.exports = {
|
||||
github: '#24292E;',
|
||||
brown: '#382D22',
|
||||
copper: '#DD792D',
|
||||
paper: '#171d26',
|
||||
divider: '#2f363d',
|
||||
},
|
||||
boxShadow: {
|
||||
outline: 'inset 0 0 0 2px rgba(0, 82, 205, 0.6)',
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 960d815f6: added docs for Nhost Run
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -28,6 +28,7 @@ Learn more about the product and features of Nhost.
|
||||
- [Authentication](/authentication)
|
||||
- [Storage](/storage)
|
||||
- [Serverless Functions](/serverless-functions)
|
||||
- [Run](/run)
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
37
docs/docs/run/2.getting_started.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Getting Started
|
||||
sidebar_label: Getting Started
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
To start with Nhost Run, you will need to create an Nhost project first. Then you can click on `Run` in the sidebar:
|
||||
|
||||

|
||||
|
||||
Then on `New Service`:
|
||||
|
||||

|
||||
|
||||
Now you can fill your [service configuration](/run/configuration):
|
||||
|
||||

|
||||
|
||||
As you configure the `Ports` section you can take note of the generated URL. You can find more information about this section under [Networking](/run/networking).
|
||||
|
||||

|
||||
|
||||
Once you are done configuring your service you can click on `Create`:
|
||||
|
||||

|
||||
|
||||
Now wait for the service to finish updating:
|
||||
|
||||

|
||||
|
||||
Finally you can visit the URL you copied before:
|
||||
|
||||

|
||||
|
||||
And profit!
|
||||
59
docs/docs/run/3_configuration.mdx
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Configuration
|
||||
sidebar_label: Configuration
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
To configure a service you can use either the dashboard or a TOML configuration file with the following format:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```
|
||||
name="mynodeapp"
|
||||
|
||||
[image]
|
||||
image="registry.eu-central-1.nhost.run/b719c4a4-6fb2-4feb-8220-c7f01ef5bfe8:0.1.1"
|
||||
|
||||
[[environment]]
|
||||
name="ENV"
|
||||
value="prod"
|
||||
|
||||
[[ports]]
|
||||
type="http"
|
||||
port=3000
|
||||
publish=true
|
||||
|
||||
[resources]
|
||||
replicas=1
|
||||
|
||||
[resources.compute]
|
||||
cpu=62
|
||||
memory=128
|
||||
|
||||
[[resources.storage]]
|
||||
name="data"
|
||||
path="/var/lib/data"
|
||||
capacity=1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
::::info
|
||||
Head to [CLI & CI deployments](/run/ci) for more details on how to deploy using a configuration file
|
||||
::::
|
||||
|
||||
The `name` of the service is used as an identifier and to generate URLs when exposing the service to the Internet. You can use any container image publicly available or you can push your own to the [Nhost registry](/run/registry).
|
||||
|
||||
All environment variables set here are exclusive to this service and will not be shared with other services or with the Nhost stack. If you are using a configuration file secrets are supported.
|
||||
|
||||
For more details about the `Ports` section head to [networking](/run/networking). You can also head to [resources](/run/resources) for more information about replicas, compute, and storage.
|
||||
119
docs/docs/run/4_networking.mdx
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Networking
|
||||
sidebar_label: Networking
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
|
||||
Nhost Run services and the main Nhost stack share the same network, facilitating direct and efficient communication. This network connectivity offers low latency, high throughput, and eliminates egress costs.
|
||||
## Connecting to the Nhost stack
|
||||
|
||||
To connect your service to the Nhost stack, use the following information:
|
||||
|
||||
- postgres: `postgres://postgres:<password>@postgres-service:5432/<subdomain>?sslmode=disable`
|
||||
- hasura: base URL is `http://hasura-service:8080`
|
||||
- hasura-auth: base URL is `http://hasura-auth-service:4000`
|
||||
- hasura-storage: base URL is `http://hasura-storage-service:5000`
|
||||
|
||||
## Connecting to your service internally
|
||||
|
||||
To connect to your own service internally from another service, follow these steps:
|
||||
|
||||
1. Expose the desired port(s).
|
||||
|
||||
Example for Redis service:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```
|
||||
[[ports]]
|
||||
type = "tcp"
|
||||
port = 6379
|
||||
publish = false
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
2. Once the port is exposed, you can connect to the service using its name and the corresponding port.
|
||||
|
||||
Example: `redis://user:password@redis:6379`
|
||||
|
||||
If needed, you can open internally more than one port by repeating the block for each one of them:
|
||||
|
||||
Example for a service exposing the ports tcp/3000 and udp/4000:
|
||||
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```
|
||||
[[ports]]
|
||||
type = "tcp"
|
||||
port = 3000
|
||||
publish = false
|
||||
|
||||
[[ports]]
|
||||
type = "udp"
|
||||
port = 4000
|
||||
publish = false
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Exposing Your Service to the Internet
|
||||
|
||||
To expose your service to the internet, follow these steps:
|
||||
|
||||
1. Update your configuration with the relevant port information:
|
||||
|
||||
Example for a nodejs service exposing an API on port 3000:
|
||||
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```
|
||||
[[ports]]
|
||||
type = "http"
|
||||
port = 3000
|
||||
publish = true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
Currently, only services of type `http` can be exposed to the internet.
|
||||
:::
|
||||
|
||||
2. Once the service of type `http` is published, you can connect to it using a URL with the following format:
|
||||
|
||||
`https://<subdomain>-<svc_name>-<port>.svc.<region>.nhost.run`
|
||||
|
||||
For example:
|
||||
|
||||
`https://zlbmqjfczuwqvsquujno-mysvc-3000.svc.eu-central-1.nhost.run`
|
||||
106
docs/docs/run/5_resources.mdx
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Resources
|
||||
sidebar_label: Resources
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
|
||||
## Compute
|
||||
|
||||
You can configure compute resources for your service like this:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```
|
||||
[resources.compute]
|
||||
cpu = 62
|
||||
memory = 128
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
If you are configuring the resources via the configuration file instead of using the dashboard, keep in mind that cpu and memory allocation need to follow the following rules:
|
||||
|
||||
* The value of the `memory` parameter must be exactly 2,048 times the value of the cpu parameter.
|
||||
* The value of the memory parameter must be within the range of 128 to 7,168 (inclusive).
|
||||
|
||||
Don't forget you can use `nhost run config-validate` command to verify your values are correct.
|
||||
|
||||
## Storage
|
||||
|
||||
By default a container's disk is ephemeral so data isn't persisted across reboots. If your service needs persistent storage you can attach one or more SSD disks by adding the following configuration:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
``` toml
|
||||
[[resources.storage]]
|
||||
name="database"
|
||||
path="/var/lib/db"
|
||||
capacity=10
|
||||
|
||||
|
||||
[[resources.storage]]
|
||||
name="data"
|
||||
path="/mnt/data"
|
||||
capacity=1
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
With the above configuration, two disks will be provided. The first disk named `database` will have a capacity of 10 GiB and will be mounted at `/var/lib/db`. The second disk named `data` will have a capacity of 1 GiB and will be mounted at ``/mnt/data`.`
|
||||
|
||||
:::warning
|
||||
Please note that renaming a disk will result in the destruction of the old disk and the creation of a new one, potentially leading to data loss.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Volume capacity can not be decreased. Once a volume with a given size has been created, its capacity can only by expanded.
|
||||
:::
|
||||
|
||||
It's important to note that disks are unique to each service and cannot be shared across multiple services.
|
||||
|
||||
|
||||
## Pausing a service
|
||||
|
||||
To pause a service, simply set its number of replicas to `0`:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="dashboard" label="dashboard" default>
|
||||
|
||||
|
||||

|
||||
|
||||
</TabItem>
|
||||
<TabItem value="toml" label="toml">
|
||||
|
||||
```toml
|
||||
[resources]
|
||||
replicas = 0
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When you pause a service, it will cease running while keeping any associated disk and registry intact.
|
||||
|
||||
:::info
|
||||
While the service is paused, computing costs will not be charged. However, storage costs will continue to apply.
|
||||
:::
|
||||
96
docs/docs/run/6_registry.mdx
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Registry
|
||||
sidebar_label: Registry
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Creating a private repository for your image
|
||||
|
||||
We provide a private image registry you can push images to with each service. To make use of it you can start by creating a service and configuring it:
|
||||
|
||||

|
||||
|
||||
Note that we are leaving the Image empty, this will auto-populate the service with the provisioned registry. We are also setting `Replicas` to 0 to avoid starting the service and incurring costs before we have pushed the image.
|
||||
|
||||
Now you can click on Create:
|
||||
|
||||

|
||||
|
||||
Now you can click on the newly created service:
|
||||
|
||||

|
||||
|
||||
And copy the image:
|
||||
|
||||

|
||||
|
||||
## Configuring docker to use Nhost's registry
|
||||
|
||||
The CLI can configure docker automatically to be able to push and pull images from the docker registry. To do so you need to run the following command:
|
||||
|
||||
```
|
||||
$ nhost docker-credentials configure
|
||||
Installing credentials helper for docker in /usr/local/bin/docker-credential-nhost-login
|
||||
I need root privileges to install the file. Please, enter your password.
|
||||
Password:
|
||||
- I am about to configure docker to authenticate with Nhost's registry. This will modify your docker config file on /Users/dbarroso/.docker/config.json. Should I continue? [y/N] y
|
||||
````
|
||||
|
||||
After you have configured the credentials helper with the command above docker should be able to interact with your images normally:
|
||||
|
||||
|
||||
```
|
||||
$ docker buildx build \
|
||||
--push \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t registry.eu-central-1.nhost.run/f02bb536-f785-4732-9eb8-d1d3664d7949:123 \
|
||||
.
|
||||
[+] Building 8.7s (11/11) FINISHED docker-container:focused_dirac
|
||||
=> [internal] load .dockerignore 0.0s
|
||||
=> => transferring context: 2B 0.0s
|
||||
=> [internal] load build definition from Dockerfile 0.0s
|
||||
=> => transferring dockerfile: 96B 0.0s
|
||||
=> [linux/arm64 internal] load metadata for docker.io/library/node:18-slim 6.4s
|
||||
=> [linux/amd64 internal] load metadata for docker.io/library/node:18-slim 6.4s
|
||||
=> [linux/arm64 1/2] FROM docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
|
||||
=> => resolve docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
|
||||
=> [internal] load build context 0.0s
|
||||
=> => transferring context: 28B 0.0s
|
||||
=> [linux/amd64 1/2] FROM docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
|
||||
=> => resolve docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
|
||||
=> CACHED [linux/arm64 2/2] ADD app.js app.js 0.0s
|
||||
=> CACHED [linux/amd64 2/2] ADD app.js app.js 0.0s
|
||||
=> exporting to image 2.2s
|
||||
=> => exporting layers 0.0s
|
||||
=> => exporting manifest sha256:b38199ab8a25d3c765cd763be8af6ea6b3542455f2e7ad37f92924538dbc2af7 0.0s
|
||||
=> => exporting config sha256:d66a5274b034a3b5698044a03bbec53dccc9e4f5f774701c59604fa17c8f0ff3 0.0s
|
||||
=> => exporting attestation manifest sha256:c5487067b094ab1d7a81a216595a0f773c55ceec95319e844cded94247e4d341 0.0s
|
||||
=> => exporting manifest sha256:f1c56d22755cae4d73ee432f11d248ca10ae4b89be3d56f0ffe95e5d7ce10542 0.0s
|
||||
=> => exporting config sha256:a532f91c5f9899a150b9c981933fa3b163879dcb5ceb48ebc132f72b564a5878 0.0s
|
||||
=> => exporting attestation manifest sha256:9e1a0207de26c335df4cee8398ada86f7b6a68ebb86e39550fe862416a10df84 0.0s
|
||||
=> => exporting manifest list sha256:f0d8521804f16280642e99c8c25bbd66f659a9a9bd7bb72cf0726dd98d9bfb00 0.0s
|
||||
=> => pushing layers 1.2s
|
||||
=> => pushing manifest for registry.eu-central-1.nhost.run/f02bb536-f785-4732-9eb8-d1d3664d7949:123@sha256:f0d8521804f16280642e99c8c25bbd66f659a9a9bd7bb72cf0726dd98d9bfb00 1.0s
|
||||
=> [auth] sharing credentials for registry.eu-central-1.nhost.run 0.0s
|
||||
|
||||
|
||||
```
|
||||
:::info
|
||||
The credentials helper will authenticate requests with the logged in user, so don't forget to authenticate with your user before trying to push or pull images. You can log in by running the command `nhost login`.
|
||||
:::
|
||||
|
||||
## Updating the image in the service configuration
|
||||
|
||||
After you have pushed your image you can click on your service again and update the configuration:
|
||||
|
||||

|
||||
|
||||
Notice we added the tag `:123` to the image that was already pre-populated and that we increased replicas to `1` to unpause the service. Don't forget to copy the URL where we are exposing the service. Now you can click on update:
|
||||
|
||||

|
||||
|
||||
Wait a few seconds until the project is done updating the new service and visit the URL we copied before:
|
||||
|
||||

|
||||
53
docs/docs/run/7_ci.mdx
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: CLI & CI Deployments
|
||||
sidebar_label: CLI & CI Deployments
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Deploying with the CLI
|
||||
|
||||
Below you can find a simple script to deploy a Nhost Run service using the CLI. This script can be leveraged on any CI or environment as long as you can install the Nhost CLI and have access to docker buildx.
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
SERVICE_ID="2503b290-249c-42f5-b89e-fd9a98980e22"
|
||||
IMAGE="registry.eu-central-1.nhost.run/2503b290-249c-42f5-b89e-fd9a98980e22"
|
||||
PAT="this-is-my-pat"
|
||||
VERSION="1.0.0"
|
||||
CONFIGURATION_FILE="nhost-service.toml"
|
||||
|
||||
# this only needs to be done once in each environment
|
||||
# but usually CIs start with a clean environment each time
|
||||
#
|
||||
# you can also login with your regular email/password
|
||||
# credentials if you prefer
|
||||
nhost login --pat $PAT
|
||||
|
||||
# this only needs to be done once in each environment
|
||||
# but usually CIs start with a clean environment each time
|
||||
nhost docker-credentials configure --no-interactive
|
||||
|
||||
docker buildx build \
|
||||
--push \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t $IMAGE:$VERSION \
|
||||
.
|
||||
|
||||
nhost run config-deploy \
|
||||
--config $CONFIGURATION \
|
||||
--service-id $SERVICE_ID
|
||||
```
|
||||
|
||||
::::info
|
||||
You can create a PAT by heading to the dashboard -> Account Settings -> Create Personal Access Token
|
||||
::::
|
||||
|
||||
## Deploy from Github Actions
|
||||
|
||||
If you prefer to deploy from GitHub actions we support a few GitHub actions you can use to build your own workflows. We also have an already-made workflow you can leverage and there is a hello-world application you can check for a more thorough example and a step-by-step demo:
|
||||
|
||||
1. [Github actions](https://github.com/marketplace?type=actions&query=nhost+)
|
||||
2. [Workflows](https://github.com/nhost-actions/workflows#build-and-release-nhost-runyaml)
|
||||
3. [Hello World](https://github.com/nhost/nhost-run-hello-world)
|
||||
4
docs/docs/run/_category_.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Run",
|
||||
"position": 9
|
||||
}
|
||||
49
docs/docs/run/index.mdx
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Overview
|
||||
sidebar_label: Overview
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
Nhost Run enables you to seamlessly incorporate your custom software within your project environment. With this feature, you can run various applications, including homegrown backend services, agents, extensions for your GraphQL API through remote schemas or actions, data-processing workloads, etc., all in close proximity to your database for low latency, speed, and efficiency.
|
||||
|
||||

|
||||
|
||||
:::info
|
||||
Currently Nhost Run is in private beta. If you are interested in using this service feel free to reach out to us via [email](mailto:support@nhost.io), [GitHub](https://github.com/nhost/nhost/issues), or [Discord](https://discord.com/invite/9V7Qb2U)
|
||||
:::
|
||||
|
||||
## Use Cases
|
||||
|
||||
Nhost Run allows you to expand and truly customize your backend in multiple ways:
|
||||
|
||||
1. Homegrown Backend Services: Deploy and execute your custom backend services within your project environment.
|
||||
2. GraphQL API Extensions: Extend your GraphQL API functionalities by incorporating remote schemas or actions.
|
||||
3. Data-Processing Workloads: Execute data-processing tasks in close proximity to your database for enhanced efficiency.
|
||||
4. OSS and third-party software: Redis, memcache, datadog agents, mysql, mongodb... anything your application needs.
|
||||
|
||||
## Advantages
|
||||
|
||||
Nhost Run offers several key advantages for running workloads alongside your project:
|
||||
|
||||
1. **Minimal Latency**: By running workloads alongside your project environment, Nhost Run reduces latency between services. This means that the communication and data exchange between different components of your project can occur quickly and efficiently.
|
||||
2. **Improved Reliability**: Nhost Run eliminates the dependency on external internet connectivity. This increased reliability ensures that your workloads continue to function even in scenarios where internet access may be limited or disrupted.
|
||||
3. **No Egress Costs**: With Nhost Run, you won't incur additional egress costs for transferring data between your project and the cloud infrastructure. This cost-saving benefit allows you to manage your expenses more effectively.
|
||||
4. **Integrated operations**: Develop, build, manage and scale your own workloads the same way you can manage your Nhost Project.
|
||||
|
||||
By leveraging Nhost Run, you can optimize the performance, reliability, and cost-efficiency of your project by running workloads alongside it.
|
||||
|
||||
## Requirements
|
||||
|
||||
Nhost Run works with container images built for the **arm architecture**. Images can be pulled from the [Nhost's private registry](/run/registry) or from any other publicly available registry.
|
||||
|
||||
## Roadmap
|
||||
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@@ -46,6 +46,16 @@ const sidebars = {
|
||||
},
|
||||
'storage',
|
||||
'serverless-functions',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Run',
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'run'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'CLI',
|
||||
|
||||
BIN
docs/static/img/run/configuration.png
vendored
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
docs/static/img/run/configure-http-port.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/run/configure-multiple-ports.png
vendored
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/static/img/run/configure-port.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/static/img/run/getting_started_1.png
vendored
Normal file
|
After Width: | Height: | Size: 817 KiB |
BIN
docs/static/img/run/getting_started_2.png
vendored
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
docs/static/img/run/getting_started_3.png
vendored
Normal file
|
After Width: | Height: | Size: 854 KiB |
BIN
docs/static/img/run/getting_started_4.png
vendored
Normal file
|
After Width: | Height: | Size: 679 KiB |
BIN
docs/static/img/run/getting_started_5.png
vendored
Normal file
|
After Width: | Height: | Size: 670 KiB |
BIN
docs/static/img/run/getting_started_6.png
vendored
Normal file
|
After Width: | Height: | Size: 617 KiB |
BIN
docs/static/img/run/getting_started_7.png
vendored
Normal file
|
After Width: | Height: | Size: 833 KiB |
BIN
docs/static/img/run/overview.png
vendored
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/static/img/run/registry_1.png
vendored
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
docs/static/img/run/registry_2.png
vendored
Normal file
|
After Width: | Height: | Size: 675 KiB |
BIN
docs/static/img/run/registry_3.png
vendored
Normal file
|
After Width: | Height: | Size: 619 KiB |
BIN
docs/static/img/run/registry_4.png
vendored
Normal file
|
After Width: | Height: | Size: 602 KiB |
BIN
docs/static/img/run/registry_5.png
vendored
Normal file
|
After Width: | Height: | Size: 791 KiB |
BIN
docs/static/img/run/registry_6.png
vendored
Normal file
|
After Width: | Height: | Size: 716 KiB |
BIN
docs/static/img/run/registry_7.png
vendored
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/static/img/run/resources_1.png
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/static/img/run/resources_2.png
vendored
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/static/img/run/resources_3.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -1,5 +1,14 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dba71483d: chore: react-apollo-example: add profile to allowedUrls
|
||||
- e819903f1: chore: remove facebook login
|
||||
- @nhost/react@2.0.30
|
||||
- @nhost/react-apollo@5.0.34
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -28,10 +28,11 @@ httpPoolSize = 100
|
||||
version = 16
|
||||
|
||||
[auth]
|
||||
version = '0.20.2'
|
||||
version = '0.21.2'
|
||||
|
||||
[auth.redirections]
|
||||
clientUrl = 'http://localhost:3000'
|
||||
clientUrl = 'https://react-apollo.example.nhost.io/'
|
||||
allowedUrls = ['https://react-apollo.example.nhost.io/profile']
|
||||
|
||||
[auth.signUp]
|
||||
enabled = true
|
||||
@@ -94,13 +95,17 @@ enabled = false
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.github]
|
||||
enabled = false
|
||||
enabled = true
|
||||
clientId = '{{ secrets.GITHUB_CLIENT_ID }}'
|
||||
clientSecret = '{{ secrets.GITHUB_CLIENT_SECRET }}'
|
||||
|
||||
[auth.method.oauth.gitlab]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.google]
|
||||
enabled = false
|
||||
enabled = true
|
||||
clientId = '{{ secrets.GOOGLE_CLIENT_ID }}'
|
||||
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
|
||||
|
||||
[auth.method.oauth.linkedin]
|
||||
enabled = false
|
||||
@@ -124,7 +129,11 @@ enabled = false
|
||||
enabled = false
|
||||
|
||||
[auth.method.webauthn]
|
||||
enabled = false
|
||||
enabled = true
|
||||
|
||||
[auth.method.webauthn.relyingParty]
|
||||
name = 'apollo-example'
|
||||
origins = ['https://react-apollo.example.nhost.io']
|
||||
|
||||
[auth.method.webauthn.attestation]
|
||||
timeout = 60000
|
||||
|
||||
12
examples/react-apollo/nhost/overlays/local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/auth/method/webauthn/relyingParty/origins/0",
|
||||
"value": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/auth/redirections/clientUrl",
|
||||
"value": "http://localhost:3000"
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FaFacebook, FaGithub, FaGoogle } from 'react-icons/fa/index.js'
|
||||
import { FaGithub, FaGoogle } from 'react-icons/fa/index.js'
|
||||
|
||||
import { useProviderLink } from '@nhost/react'
|
||||
|
||||
import AuthLink from './AuthLink'
|
||||
|
||||
export default function OauthLinks() {
|
||||
const { github, google, facebook } = useProviderLink({ redirectTo: window.location.origin })
|
||||
const { github, google } = useProviderLink({ redirectTo: window.location.origin })
|
||||
return (
|
||||
<>
|
||||
<AuthLink leftIcon={<FaGithub />} link={github} color="#333">
|
||||
@@ -14,9 +14,6 @@ export default function OauthLinks() {
|
||||
<AuthLink leftIcon={<FaGoogle />} link={google} color="#de5246">
|
||||
Continue with Google
|
||||
</AuthLink>
|
||||
<AuthLink leftIcon={<FaFacebook />} link={facebook} color="#3b5998">
|
||||
Continue with Facebook
|
||||
</AuthLink>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RemoveSecurityKeyMutation, SecurityKeysQuery } from 'src/generated'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { ActionIcon, Button, Card, SimpleGrid, Table, TextInput, Title } from '@mantine/core'
|
||||
import { useInputState } from '@mantine/hooks'
|
||||
import { showNotification } from '@mantine/notifications'
|
||||
import { useAddSecurityKey, useUserId } from '@nhost/react'
|
||||
import { useAuthQuery } from '@nhost/react-apollo'
|
||||
|
||||
@@ -42,9 +43,14 @@ export const SecurityKeys: React.FC = () => {
|
||||
|
||||
const addKey = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const { key, error } = await add(nickname)
|
||||
if (error) {
|
||||
const { key, isError, error } = await add(nickname)
|
||||
if (isError) {
|
||||
console.log(error)
|
||||
showNotification({
|
||||
color: 'red',
|
||||
title: 'Error',
|
||||
message: error?.message || null
|
||||
})
|
||||
} else {
|
||||
setNickname('')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @nhost-examples/serverless-functions
|
||||
|
||||
## 0.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 45759c4d4: fix(stripe-graphql-js): fix stripe GraphQL extension export issue in serverless functions
|
||||
- Updated dependencies [45759c4d4]
|
||||
- @nhost/stripe-graphql-js@1.0.5
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -17,6 +17,4 @@ https://github.com/nhost/nhost/tree/main/integrations/stripe-graphql-js
|
||||
|
||||
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
|
||||
|
||||
const server = createStripeGraphQLServer()
|
||||
|
||||
export default server
|
||||
export default createStripeGraphQLServer()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[global]
|
||||
[[global.environment]]
|
||||
name='STRIPE_SECRET_KEY'
|
||||
value='{{ secrets.STRIPE_SECRET_KEY }}'
|
||||
|
||||
[hasura]
|
||||
version = 'v2.25.1-ce'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nhost-examples/serverless-functions",
|
||||
"private": true,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-yoga/node": "^2.13.13",
|
||||
"@nhost/stripe-graphql-js": "^1.0.2",
|
||||
"@nhost/stripe-graphql-js": "^1.0.5",
|
||||
"@pothos/core": "^3.21.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"graphql": "15.7.2",
|
||||
|
||||
13
examples/sveltekit/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
1
examples/sveltekit/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
hoist-pattern[]=!@nhost/nhost-js
|
||||
5
examples/sveltekit/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost-examples/sveltekit': minor
|
||||
---
|
||||
|
||||
feat: add nhost with sveltekit example project
|
||||
44
examples/sveltekit/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Nhost with SvelteKit Example
|
||||
|
||||
## 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. Go to the SvelteKit example folder
|
||||
|
||||
```sh
|
||||
cd examples/sveltekit
|
||||
```
|
||||
|
||||
4. Create a `.env` file and set the subdomain and region of your Nhost project. When running locally with the CLI, set the subdomain to `local`.
|
||||
|
||||
```sh
|
||||
PUBLIC_NHOST_SUBDOMAIN=
|
||||
PUBLIC_NHOST_REGION=
|
||||
```
|
||||
|
||||
5. Terminal 1: Start Nhost
|
||||
|
||||
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
|
||||
|
||||
```sh
|
||||
nhost up
|
||||
```
|
||||
|
||||
6. Terminal 2: Start the SvelteKit dev server
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
17
examples/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
|
||||
}
|
||||
45
examples/sveltekit/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@nhost-examples/sveltekit",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"install-browsers": "pnpm dlx playwright@1.31.0 install --with-deps",
|
||||
"add-nhost-js": "pnpm add @nhost/nhost-js --ignore-workspace",
|
||||
"test": "pnpm install-browsers && pnpm add-nhost-js && pnpm dlx playwright@1.31.0 test",
|
||||
"lint": "eslint .",
|
||||
"postinstall": "pnpm add-nhost-js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/nhost-js": "2.2.13",
|
||||
"@playwright/test": "^1.31.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.26.0",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.3.0",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.1",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"playwright": "^1.37.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
57
examples/sveltekit/playwright.config.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// @ts-check
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:5173',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] }
|
||||
}
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'pnpm dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
})
|
||||
179
examples/sveltekit/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,179 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
graphql:
|
||||
specifier: ^16.7.1
|
||||
version: 16.8.0
|
||||
|
||||
devDependencies:
|
||||
'@nhost/nhost-js':
|
||||
specifier: 2.2.13
|
||||
version: 2.2.13(graphql@16.8.0)
|
||||
|
||||
packages:
|
||||
|
||||
/@graphql-typed-document-node/core@3.2.0(graphql@16.8.0):
|
||||
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
|
||||
peerDependencies:
|
||||
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
|
||||
dependencies:
|
||||
graphql: 16.8.0
|
||||
dev: true
|
||||
|
||||
/@nhost/graphql-js@0.1.4(graphql@16.8.0):
|
||||
resolution: {integrity: sha512-IPHuGOf4iQrFsxG7Rh5jCCZzPCN9JkvldFww4Fz1lCVi9ZQNEaGaawIP5gBuBHeYIuALeaK1wVYKPc7vJ/euCA==}
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
'@graphql-typed-document-node/core': 3.2.0(graphql@16.8.0)
|
||||
graphql: 16.8.0
|
||||
isomorphic-unfetch: 3.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-auth-js@2.1.7:
|
||||
resolution: {integrity: sha512-dbi8zrmuE3xSlA7WMNyrZzVPLKYSBUlKtUjob+axhts0+4TsqsB7NvdLXrhM0fYjRY5SFUtN2JLs+EWDGKAoLQ==}
|
||||
dependencies:
|
||||
'@simplewebauthn/browser': 6.2.2
|
||||
fetch-ponyfill: 7.1.0
|
||||
js-cookie: 3.0.5
|
||||
jwt-decode: 3.1.2
|
||||
xstate: 4.38.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-storage-js@2.2.2:
|
||||
resolution: {integrity: sha512-GMeB1m6YKZMZDSO6UtmgXYWxsFUNlphIZH9JeiDX+6UTmbd62UlDmhBcmx2OGy/tvv57D+HbohpV6AY9S6QL4w==}
|
||||
dependencies:
|
||||
fetch-ponyfill: 7.1.0
|
||||
form-data: 4.0.0
|
||||
xstate: 4.38.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/nhost-js@2.2.13(graphql@16.8.0):
|
||||
resolution: {integrity: sha512-HU9eOpkVBGGMHsRThGyl654Y9W8bksSEnyEvDojUg76+3ngOn2ebrasXR/94YoR206soEzq3v4b0OKmn/1jlnQ==}
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
'@nhost/graphql-js': 0.1.4(graphql@16.8.0)
|
||||
'@nhost/hasura-auth-js': 2.1.7
|
||||
'@nhost/hasura-storage-js': 2.2.2
|
||||
graphql: 16.8.0
|
||||
isomorphic-unfetch: 3.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@simplewebauthn/browser@6.2.2:
|
||||
resolution: {integrity: sha512-VUtne7+s6BmW4usnbitjZEI1VNT/PNh6bYg+AI4OMdfpo5z+yAq+6iVAWBJlIUGVk5InetEQvTUp6OefBam8qg==}
|
||||
dev: true
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/fetch-ponyfill@7.1.0:
|
||||
resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.12
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/graphql@16.8.0:
|
||||
resolution: {integrity: sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
/isomorphic-unfetch@3.1.0:
|
||||
resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.12
|
||||
unfetch: 4.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/jwt-decode@3.1.2:
|
||||
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
|
||||
dev: true
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.6.12:
|
||||
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/unfetch@4.2.0:
|
||||
resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
|
||||
/xstate@4.38.2:
|
||||
resolution: {integrity: sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==}
|
||||
dev: true
|
||||
7
examples/sveltekit/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
13
examples/sveltekit/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {}
|
||||
interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export { };
|
||||
|
||||