Compare commits

..

46 Commits

Author SHA1 Message Date
github-actions[bot]
b9316bb668 chore: update versions (#2041)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@0.17.11

### Patch Changes

- bd4d0c270: chore(dashboard):add postgres 14.6-20230613-1 to the
version selector

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-14 14:29:07 +02:00
David Barroso
bd4d0c2708 chore(dashboard):add postgres 14.6-20230613-1 to the version selector (#2039) 2023-06-14 14:15:12 +02:00
Szilárd Dóró
d81c52209b Merge pull request #2036 from nhost/chore/bump-nhost-cli
chore(nix): bump Nhost CLI and Node.js versions
2023-06-13 16:05:04 +02:00
Szilárd Dóró
72744b3082 chore: remove duplicate nhost package 2023-06-13 14:43:04 +02:00
Szilárd Dóró
ff4efe2712 chore: bump Node version in workspace 2023-06-13 14:42:28 +02:00
Szilárd Dóró
2982b90469 chore: bump Nhost CLI version 2023-06-13 14:35:21 +02:00
Szilárd Dóró
428a5df038 Merge pull request #2034 from nhost/changeset-release/main
chore: update versions
2023-06-13 13:40:44 +02:00
github-actions[bot]
f79bf784b5 chore: update versions 2023-06-13 11:27:56 +00:00
Szilárd Dóró
3b7449ac08 Merge pull request #2035 from nhost/fix/reset-password
fix(dashboard): don't break the password reset flow
2023-06-13 13:26:46 +02:00
Szilárd Dóró
37bbfdb7ae fix: use system colors when storage is empty 2023-06-13 13:24:30 +02:00
Szilárd Dóró
eb570d2d09 fix: don't break linter 2023-06-13 13:22:09 +02:00
Szilárd Dóró
c8c2a10b2d fix: dashboard: don't break the password reset flow 2023-06-13 13:09:36 +02:00
Szilárd Dóró
92c79eb2fb Merge pull request #2033 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-06-13 10:29:58 +02:00
Szilárd Dóró
e70b45498d chore: add changeset 2023-06-13 09:57:17 +02:00
renovate[bot]
2e1ecfa731 chore(deps): update react monorepo 2023-06-13 06:40:47 +00:00
Szilárd Dóró
8d323a7762 Merge pull request #2031 from nhost/changeset-release/main
chore: update versions
2023-06-13 08:38:01 +02:00
github-actions[bot]
8aa0ff936a chore: update versions 2023-06-13 06:05:58 +00:00
Szilárd Dóró
c6806d60c7 Merge pull request #1995 from nhost/chore/remove-password-input
chore(dashboard): remove password input from project creation
2023-06-13 08:04:39 +02:00
Szilárd Dóró
a13eb25ebc Merge pull request #2021 from nhost/renovate/react-monorepo 2023-06-12 16:59:13 +02:00
Szilárd Dóró
228d8a0686 fix: don't break build 2023-06-12 16:07:08 +02:00
Szilárd Dóró
0de1bc7ce3 Merge branch 'main' into renovate/react-monorepo 2023-06-12 16:01:01 +02:00
Szilárd Dóró
6a94cad04b Merge pull request #2018 from nhost/renovate/vitest-monorepo
chore(deps): update vitest monorepo to ^0.32.0
2023-06-12 15:49:22 +02:00
Szilárd Dóró
8643d25cc8 Merge branch 'renovate/react-monorepo' of https://github.com/nhost/nhost into renovate/react-monorepo 2023-06-12 15:28:54 +02:00
Szilárd Dóró
e820f11dda Merge branch 'renovate/vitest-monorepo' of https://github.com/nhost/nhost into renovate/vitest-monorepo 2023-06-12 15:24:50 +02:00
Szilárd Dóró
3555ab2b71 chore: add changeset and swap coverage dependency 2023-06-12 15:23:40 +02:00
renovate[bot]
6e41d58131 chore(deps): update vitest monorepo to ^0.32.0 2023-06-12 13:15:35 +00:00
renovate[bot]
6cf3beae1c chore(deps): update dependency @types/react to v18.2.11 2023-06-12 13:14:46 +00:00
Szilárd Dóró
022b76e784 chore: add changeset 2023-06-12 15:13:41 +02:00
Szilárd Dóró
2fbe88f806 Merge pull request #2032 from nhost/feat/download-backups
feat(dashboard): add download button to backups
2023-06-12 15:12:19 +02:00
Szilárd Dóró
9457bc32ca chore: dashboard: simplify restoration modal 2023-06-12 14:07:42 +02:00
Szilárd Dóró
3de2639ae9 feat: dashboard: add Hasura v2.27.0-ce to the version selector 2023-06-12 13:22:23 +02:00
Szilárd Dóró
c43e549224 feat: dashboard: add download button to backups 2023-06-12 13:12:42 +02:00
renovate[bot]
fc6fe5007b chore(deps): update vitest monorepo to ^0.32.0 2023-06-12 10:44:57 +00:00
renovate[bot]
829febf33b chore(deps): update dependency @types/react to v18.2.11 2023-06-12 10:44:09 +00:00
David Barroso
ae99ba14b9 docs: added documentation on overlays (#2004) 2023-06-12 12:41:43 +02:00
Szilárd Dóró
a158dc3a17 Merge pull request #2030 from nhost/chore/bump-turbo-pnpm
chore: bump turbo and pnpm
2023-06-12 11:59:06 +02:00
Szilárd Dóró
8420550990 chore: bump turbo and pnpm 2023-06-12 11:41:19 +02:00
Szilárd Dóró
156667cdbd Merge pull request #2027 from nhost/chore/gh-actions-node
fix: revert Node to v16
2023-06-12 11:20:02 +02:00
Szilárd Dóró
7d388a8c91 fix: revert Node to v16 2023-06-12 10:41:26 +02:00
Szilárd Dóró
dfa8776b2b chore: show loading state 2023-06-02 11:18:28 +02:00
Szilárd Dóró
1b9f15cb67 chore: improve password reset UX 2023-06-02 11:14:19 +02:00
Szilárd Dóró
b683615269 chore: confirm route change on database form 2023-06-02 10:53:33 +02:00
Szilárd Dóró
a60ca2f6f5 chore: update info message, update reset button 2023-06-01 16:45:10 +02:00
Szilárd Dóró
14a2ead79f chore: unify Alert component's styling 2023-06-01 16:24:29 +02:00
Szilárd Dóró
b625a6b4d4 chore: cleanup unused code 2023-06-01 16:24:12 +02:00
Szilárd Dóró
fd12aa0a8d chore: remove password input 2023-06-01 16:23:57 +02:00
47 changed files with 1600 additions and 1087 deletions

View File

@@ -14,7 +14,7 @@ runs:
steps:
- uses: pnpm/action-setup@v2.2.4
with:
version: 8.5.1
version: 8.6.2
run_install: false
- name: Get pnpm cache directory
id: pnpm-cache-dir
@@ -26,10 +26,10 @@ runs:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-node-
- name: Use Node.js 16
- name: Use Node.js v16
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- shell: bash
name: Install packages
run: pnpm install --frozen-lockfile

4
.gitignore vendored
View File

@@ -62,3 +62,7 @@ todo.md
# Nhost CLI data
.nhost
# Nix
.envrc
.direnv/

View File

@@ -1,5 +1,28 @@
# @nhost/dashboard
## 0.17.11
### Patch Changes
- bd4d0c270: chore(dashboard):add postgres 14.6-20230613-1 to the version selector
## 0.17.10
### Patch Changes
- c8c2a10b2: fix(database): don't break the password reset flow
- e70b45498: chore(deps): bump `@types/react` to `v18.2.12` and `@types/react-dom` to `v18.2.5`
## 0.17.9
### Patch Changes
- 842055099: chore(deps): bump `turbo` to `v1.10.3` and `pnpm` to `v8.6.2`
- fd12aa0a8: chore(projects): remove the postgres password input from the project creation screen
- 022b76e78: chore(deps): bump `@types/react` to `v18.2.11`
- 3555ab2b7: chore(deps): bump `vitest` monorepo to `v0.32.0`
- c43e54922: feat(backups): add download button to backups
## 0.17.8
### Patch Changes

View File

@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
RUN yarn global add turbo@1.10.1
RUN yarn global add turbo@1.10.3
COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker
@@ -29,7 +29,7 @@ ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL_
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
RUN yarn global add pnpm@8.5.1
RUN yarn global add pnpm@8.6.2
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-*.yaml .

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.17.8",
"version": "0.17.11",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -104,15 +104,15 @@
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.2.8",
"@types/react-dom": "18.2.4",
"@types/react": "18.2.12",
"@types/react-dom": "18.2.5",
"@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-c8": "^0.31.0",
"@vitest/coverage-v8": "^0.32.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -146,7 +146,7 @@
"tsconfig-paths-webpack-plugin": "^4.0.0",
"vite": "^4.0.2",
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.31.0"
"vitest": "^0.32.0"
},
"browserslist": {
"production": [

View File

@@ -7,7 +7,6 @@ import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { isK8SPostgresEnabledInCurrentEnvironment } from '@/utils/helpers';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -135,15 +134,13 @@ export default function SettingsSidebar({
>
Compute Resources
</SettingsNavLink>
{isK8SPostgresEnabledInCurrentEnvironment && (
<SettingsNavLink
href="/database"
exact={false}
onClick={handleSelect}
>
Database
</SettingsNavLink>
)}
<SettingsNavLink
href="/database"
exact={false}
onClick={handleSelect}
>
Database
</SettingsNavLink>
<SettingsNavLink
href="/hasura"
exact={false}

View File

@@ -1,6 +1,6 @@
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
import { styled } from '@mui/material';
export interface AlertProps extends BoxProps {
/**
@@ -11,19 +11,25 @@ export interface AlertProps extends BoxProps {
severity?: 'info' | 'success' | 'warning' | 'error';
}
const StyledBox = styled(Box)(({ theme }) => ({
borderRadius: 4,
padding: theme.spacing(1.5, 2),
textAlign: 'center',
fontSize: theme.typography.pxToRem(15),
lineHeight: theme.typography.pxToRem(22),
'@media (prefers-reduced-motion: no-preference)': {
transition: theme.transitions.create('background-color'),
},
}));
export default function Alert({
severity = 'info',
children,
className,
sx,
...props
}: AlertProps) {
return (
<Box
className={twMerge(
'rounded-sm+ bg-opacity-20 p-4 text-center text-sm+ motion-safe:transition-colors',
className,
)}
<StyledBox
sx={[
...(Array.isArray(sx) ? sx : [sx]),
severity === 'error' && {
@@ -43,6 +49,6 @@ export default function Alert({
{...props}
>
{children}
</Box>
</StyledBox>
);
}

View File

@@ -1,5 +0,0 @@
{
"k8s-postgres": {
"enabled": ["dev", "staging", "production"]
}
}

View File

@@ -1,5 +1,6 @@
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import type { InputProps } from '@/components/ui/v2/Input';
@@ -91,28 +92,33 @@ export default function DatabaseConnectionInfo() {
disabled
value={inputValue}
className={className}
slotProps={{ inputRoot: { className: '!pr-8 truncate' } }}
fullWidth
hideEmptyHelperText
endAdornment={
name !== 'postgresPassword' && (
<InputAdornment position="end" className="absolute right-2">
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(inputValue as string, `${label}`);
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
)
<InputAdornment position="end" className="absolute right-2">
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(inputValue as string, `${label}`);
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
}
/>
),
)}
<Alert severity="info" className="col-span-6 text-left">
To connect to the Postgres database directly, generate a new password,
securely save it, and then modify your connection string with the newly
created password.
</Alert>
</SettingsContainer>
);
}

View File

@@ -31,6 +31,7 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_POSTGRES_VERSIONS = [
'14.6-20230613-1',
'14.6-20230525',
'14.6-20230406-2',
'14.6-20230406-1',

View File

@@ -1,3 +1,4 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -6,30 +7,27 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { generateRandomDatabasePassword } from '@/features/database/common/utils/generateRandomDatabasePassword';
import type { ResetDatabasePasswordFormValues } from '@/features/database/settings/utils/resetDatabasePasswordValidationSchema';
import { resetDatabasePasswordValidationSchema } from '@/features/database/settings/utils/resetDatabasePasswordValidationSchema';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useResetPostgresPasswordMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useResetDatabasePasswordMutation } from '@/generated/graphql';
import { useLeaveConfirm } from '@/hooks/useLeaveConfirm';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { alpha } from '@mui/system';
import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface ResetDatabasePasswordFormValues {
/**
* The new password to set for the database.
*/
databasePassword: string;
}
export default function ResetDatabasePasswordSettings() {
const [updateApplication] = useUpdateApplicationMutation();
const [resetPassword, { loading: resetPasswordLoading }] =
useResetDatabasePasswordMutation();
const { maintenanceActive } = useUI();
const user = useUserData();
const { currentProject } = useCurrentWorkspaceAndProject();
const { openAlertDialog } = useDialog();
const form = useForm<ResetDatabasePasswordFormValues>({
reValidateMode: 'onSubmit',
@@ -46,41 +44,36 @@ export default function ResetDatabasePasswordSettings() {
setValue,
getValues,
register,
formState: { errors, isDirty, isSubmitting },
formState: { errors, dirtyFields, isSubmitting },
} = form;
const [resetPostgresPasswordMutation] = useResetPostgresPasswordMutation();
const user = useUserData();
const { currentProject } = useCurrentWorkspaceAndProject();
const isDirty = Object.keys(dirtyFields).length > 0;
const handleGenerateRandomPassword = () => {
useLeaveConfirm({ isDirty });
function handleGenerateRandomPassword() {
const newRandomDatabasePassword = generateRandomDatabasePassword();
triggerToast('New random database password generated.');
triggerToast(
'Random database password was generated and copied to clipboard. Submit the form to save it.',
);
copy(newRandomDatabasePassword);
setValue('databasePassword', newRandomDatabasePassword, {
shouldDirty: true,
});
};
}
const handleChangeDatabasePassword = async (
values: ResetDatabasePasswordFormValues,
) => {
async function handleChangeDatabasePassword(
formValues: ResetDatabasePasswordFormValues,
) {
try {
await resetPostgresPasswordMutation({
variables: {
appID: currentProject.id,
newPassword: values.databasePassword,
},
});
await updateApplication({
await resetPassword({
variables: {
appId: currentProject.id,
app: {
postgresPassword: values.databasePassword,
},
newPassword: formValues.databasePassword,
},
});
form.reset(values);
form.reset(formValues);
triggerToast(
`The database password for ${currentProject.name} has been updated successfully.`,
@@ -93,24 +86,45 @@ export default function ResetDatabasePasswordSettings() {
`An error occurred while trying to update the database password: ${currentProject.name} (${user.email}): ${e.message}`,
);
}
};
}
function handleSubmit(formValues: ResetDatabasePasswordFormValues) {
openAlertDialog({
title: 'Confirm Change',
payload: 'Are you sure you want to change the database password?',
props: {
primaryButtonColor: 'error',
primaryButtonText: 'Confirm',
onPrimaryAction: () => handleChangeDatabasePassword(formValues),
},
});
}
return (
<FormProvider {...form}>
<Form onSubmit={handleChangeDatabasePassword}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Reset Password"
description="This password is used for accessing your database."
submitButtonText="Reset"
description="This password will be used for accessing your database."
submitButtonText="Save"
slotProps={{
root: {
sx: { borderColor: (theme) => theme.palette.error.main },
sx: {
borderColor: (theme) =>
isDirty
? theme.palette.error.main
: alpha(theme.palette.error.main, 0.5),
'@media (prefers-reduced-motion: no-preference)': {
transition: (theme) =>
theme.transitions.create('border-color'),
},
},
},
submitButton: {
variant: 'contained',
color: 'error',
variant: isDirty ? 'contained' : 'outlined',
color: isDirty ? 'error' : 'secondary',
disabled: !isDirty || maintenanceActive,
loading: isSubmitting,
loading: isSubmitting || resetPasswordLoading,
},
}}
className="grid grid-flow-row pb-4"
@@ -126,6 +140,7 @@ export default function ResetDatabasePasswordSettings() {
hideEmptyHelperText
slotProps={{
input: { className: 'lg:w-1/2' },
inputRoot: { className: '!pr-8' },
helperText: { component: 'div' },
}}
helperText={

View File

@@ -1,2 +1 @@
export * from './ResetDatabasePasswordSettings';
export { default as ResetDatabasePasswordSettings } from './ResetDatabasePasswordSettings';

View File

@@ -0,0 +1,3 @@
mutation ResetDatabasePassword($appId: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appId, newPassword: $newPassword)
}

View File

@@ -16,4 +16,8 @@ export const resetDatabasePasswordValidationSchema = yup.object().shape({
.minUppercase(1),
});
export type ResetDatabasePasswordFormValues = yup.InferType<
typeof resetDatabasePasswordValidationSchema
>;
export default resetDatabasePasswordValidationSchema;

View File

@@ -31,6 +31,7 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_HASURA_VERSIONS = [
'v2.27.0-ce',
'v2.25.1-ce',
'v2.25.0-ce',
'v2.24.1-ce',

View File

@@ -0,0 +1,72 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Table } from '@/components/ui/v2/Table';
import { TableBody } from '@/components/ui/v2/TableBody';
import { TableCell } from '@/components/ui/v2/TableCell';
import { TableContainer } from '@/components/ui/v2/TableContainer';
import { TableHead } from '@/components/ui/v2/TableHead';
import { TableRow } from '@/components/ui/v2/TableRow';
import { Text } from '@/components/ui/v2/Text';
import { BackupListItem } from '@/features/projects/backups/components/BackupListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useGetApplicationBackupsQuery } from '@/utils/__generated__/graphql';
export default function BackupList() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetApplicationBackupsQuery({
variables: { appId: currentProject.id },
});
if (loading) {
return (
<ActivityIndicator
delay={500}
className="my-5"
label="Loading backups..."
/>
);
}
if (error) {
throw error;
}
const { backups } = data.app;
return (
<TableContainer sx={{ backgroundColor: 'background.paper' }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Size</TableCell>
<TableCell>Backed up</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{backups.length === 0 && (
<TableRow>
<TableCell>
<Text className="text-xs" color="secondary">
No backups are available.
</Text>
</TableCell>
<TableCell />
<TableCell />
<TableCell />
</TableRow>
)}
{backups.map((backup) => (
<BackupListItem
key={backup.id}
backup={backup}
projectId={currentProject.id}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
}

View File

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

View File

@@ -0,0 +1,94 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { TableCell } from '@/components/ui/v2/TableCell';
import { TableRow } from '@/components/ui/v2/TableRow';
import { RestoreBackupModal } from '@/features/projects/backups/components/RestoreBackupModal';
import type { Backup } from '@/types/application';
import { prettifySize } from '@/utils/prettifySize';
import { triggerToast } from '@/utils/toast';
import { useGetBackupPresignedUrlLazyQuery } from '@/utils/__generated__/graphql';
import { format, formatDistanceStrict, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
export interface BackupListItemProps {
/**
* Project ID.
*/
projectId: string;
/**
* Backup data.
*/
backup: Backup;
}
export default function BackupListItem({
projectId,
backup,
}: BackupListItemProps) {
const { id, createdAt, size } = backup;
const { openDialog, closeDialog } = useDialog();
const [fetchPresignedUrl, { loading: loadingPresignedUrl }] =
useGetBackupPresignedUrlLazyQuery({
variables: {
appId: projectId,
backupId: id,
},
});
async function downloadBackup() {
const { data: presignedUrlData, error } = await fetchPresignedUrl();
if (error) {
triggerToast(
'An error occurred while fetching the presigned URL. Please try again later.',
);
return;
}
if (typeof window === 'undefined') {
return;
}
window.open(presignedUrlData.getBackupPresignedUrl.url, '_blank');
}
function restoreBackup() {
openDialog({
title: 'Restore Backup',
component: <RestoreBackupModal backup={backup} close={closeDialog} />,
});
}
return (
<TableRow>
<TableCell className="text-xs">
{format(parseISO(createdAt), 'yyyy-MM-dd HH:mm:ss')}
</TableCell>
<TableCell className="text-xs">{prettifySize(size)}</TableCell>
<TableCell className="text-xs">
{formatDistanceStrict(new Date(createdAt), new Date(), {
addSuffix: true,
})}
</TableCell>
<TableCell
className={twMerge(
'grid grid-flow-col justify-end gap-2',
!loadingPresignedUrl && 'pl-8',
)}
>
<Button
variant="borderless"
onClick={downloadBackup}
loading={loadingPresignedUrl}
>
Download
</Button>
<Button variant="borderless" onClick={restoreBackup}>
Restore
</Button>
</TableCell>
</TableRow>
);
}

View File

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

View File

@@ -3,38 +3,38 @@ import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Backup } from '@/types/application';
import { triggerToast } from '@/utils/toast';
import { useRestoreApplicationDatabaseMutation } from '@/utils/__generated__/graphql';
import { formatISO9075 } from 'date-fns';
import { format, parseISO } from 'date-fns';
import { useState } from 'react';
export interface RestoreBackupModalModalProps {
export interface RestoreBackupModalProps {
/**
* Call this function to imperatively close the modal.
*/
close: any;
close: VoidFunction;
/**
* Arbitrary data passed down to the modal.
*
* Backup data.
*/
data: any;
backup: Backup;
}
export default function RestoreBackupModal({
close,
data,
}: RestoreBackupModalModalProps) {
const { id: backupId, createdAt } = data;
backup,
}: RestoreBackupModalProps) {
const { id: backupId, createdAt } = backup;
const [isSure, setIsSure] = useState(false);
const [mutationIsCompleted, setMutationIsCompleted] = useState(false);
const [restoreCompleted, setRestoreCompleted] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [restoreApplicationDatabase, { loading }] =
useRestoreApplicationDatabaseMutation();
const handleSubmit = async () => {
setMutationIsCompleted(false);
async function handleSubmit() {
setRestoreCompleted(false);
try {
await restoreApplicationDatabase({
variables: {
@@ -43,55 +43,49 @@ export default function RestoreBackupModal({
},
});
} catch (error) {
setMutationIsCompleted(false);
setRestoreCompleted(false);
triggerToast('Database backup restoration failed');
return;
}
setMutationIsCompleted(true);
setRestoreCompleted(true);
triggerToast('Database backup successfully scheduled for restoration.');
};
}
if (mutationIsCompleted) {
if (restoreCompleted) {
return (
<Box className="w-modal rounded-lg p-6">
<div className="flex flex-col">
<Text className="text-center text-lg font-medium">
The backup has been restored successfully.
</Text>
<Box className="grid grid-flow-row gap-4 px-6 pb-6">
<Text>The backup has been restored successfully.</Text>
<Button className="mt-5" onClick={close}>
OK
</Button>
</div>
<Button onClick={close}>OK</Button>
</Box>
);
}
return (
<Box className="w-modal rounded-lg px-6 py-6 text-left">
<div className="flex flex-col">
<Text className="text-center text-lg font-medium">
Restore Database Backup
</Text>
<Text className="mt-2 text-center font-normal">
You current database will be deleted, and the backup created{' '}
<span className="font-semibold">
{formatISO9075(new Date(createdAt))}
</span>{' '}
will be restored.
</Text>
<Box className="grid grid-flow-row gap-2 px-6 pb-6">
<Text>
You current database will be deleted, and the backup created at{' '}
<span className="font-semibold">
{format(parseISO(createdAt), 'yyyy-MM-dd HH:mm:ss')}
</span>{' '}
will be restored.
</Text>
<Box className="my-4 border-y py-2 px-2">
<Checkbox
checked={isSure}
onChange={(_event, checked) => setIsSure(checked)}
label="I'm sure I want to restore this backup."
/>
</Box>
<Button onClick={handleSubmit} disabled={!isSure} loading={loading}>
Restore
</Button>
</div>
<Box className="pt-1 pb-2.5">
<Checkbox
checked={isSure}
onChange={(_event, checked) => setIsSure(checked)}
label="I'm sure I want to restore this backup"
/>
</Box>
<Button onClick={handleSubmit} disabled={!isSure} loading={loading}>
Restore
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</Box>
);
}

View File

@@ -0,0 +1,6 @@
fragment Backup on backups {
id
size
createdAt
completedAt
}

View File

@@ -1,10 +1,7 @@
query getApplicationBackups($appId: uuid!) {
app(id: $appId) {
backups(order_by: { createdAt: desc }) {
id
size
createdAt
completedAt
...Backup
}
}
}

View File

@@ -0,0 +1,14 @@
query GetBackupPresignedUrl(
$appId: String!
$backupId: String!
$expireInMinutes: Int
) {
getBackupPresignedUrl: getBackupPresignedURL(
appID: $appId
backupID: $backupId
expireInMinutes: $expireInMinutes
) {
url
expiresAt: expires_at
}
}

View File

@@ -49,7 +49,7 @@ export default function StagingMetadata({
return (
isDevOrStaging() && (
<div className="mx-auto mt-10 max-w-sm">
<Box className="mx-auto flex flex-col rounded-md border p-5 text-center">
<Box className="mx-auto grid grid-flow-row justify-items-center rounded-md border p-5 text-center">
<Status status={StatusEnum.Deploying}>Internal info</Status>
{children}
</Box>

View File

@@ -1,3 +0,0 @@
mutation resetPostgresPassword($appID: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appID, newPassword: $newPassword)
}

View File

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

View File

@@ -0,0 +1,48 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export interface UseLeaveConfirmProps {
/**
* Whether the form is dirty or not.
*/
isDirty?: boolean;
}
/**
* This hook will show a confirmation dialog when the user tries to leave the
* current page while the form is dirty.
*/
export default function useLeaveConfirm({ isDirty }: UseLeaveConfirmProps) {
const router = useRouter();
const { openAlertDialog } = useDialog();
const [isConfirmed, setConfirmed] = useState(false);
useEffect(() => {
function onRouteChangeStart(route: string) {
if (!isDirty || isConfirmed) {
return;
}
openAlertDialog({
title: 'Unsaved changes',
payload:
'You have unsaved local changes. Are you sure you want to discard them?',
props: {
primaryButtonColor: 'error',
primaryButtonText: 'Discard',
onPrimaryAction: () => {
setConfirmed(true);
router.push(route);
},
},
});
throw new Error('Route change aborted');
}
router.events.on('routeChangeStart', onRouteChangeStart);
return () => router.events.off('routeChangeStart', onRouteChangeStart);
}, [isConfirmed, isDirty, openAlertDialog, router, router.events]);
}

View File

@@ -1,150 +1,13 @@
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Chip } from '@/components/ui/v2/Chip';
import { Text } from '@/components/ui/v2/Text';
import { RestoreBackupModal } from '@/features/projects/backups/components/RestoreBackupModal';
import { BackupList } from '@/features/projects/backups/components/BackupList';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useGetApplicationBackupsQuery } from '@/generated/graphql';
import { prettifySize } from '@/utils/prettifySize';
import { formatDistanceStrict, formatISO9075 } from 'date-fns';
import type { ReactElement } from 'react';
import { useState } from 'react';
export type Backup = {
id: string;
createdAt: string;
size: number;
};
function BackupsHeader() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { plan } = currentProject;
return (
<div className="flex flex-row place-content-between">
<div>
<Text className="text-2xl font-medium" variant="h1">
Backups
</Text>
</div>
<Chip
color={plan.isFree ? 'default' : 'success'}
label={plan.isFree ? 'Off' : 'Live'}
size="small"
/>
</div>
);
}
function BackupRow({ backup }: any) {
const { id, createdAt, size } = backup;
const [restoreModalOpen, setRestoreModalOpen] = useState(false);
return (
<>
{restoreModalOpen && (
<Modal
showModal={restoreModalOpen}
close={() => setRestoreModalOpen(false)}
Component={RestoreBackupModal}
data={{ id, createdAt }}
/>
)}
<Box className="flex flex-row place-content-between py-3">
<Text className="w-drop self-center text-xs font-medium">
{formatISO9075(new Date(createdAt))}
</Text>
<Text className="w-drop self-center text-xs font-medium">
{prettifySize(size)}
</Text>
<Text className="w-drop self-center text-xs font-medium">
{formatDistanceStrict(new Date(createdAt), new Date(), {
addSuffix: true,
})}
</Text>
<div className="w-20">
<Button
variant="borderless"
onClick={() => setRestoreModalOpen(true)}
>
Restore
</Button>
</div>
</Box>
</>
);
}
function BackupsTable() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetApplicationBackupsQuery({
variables: { appId: currentProject.id },
});
if (loading) {
return (
<ActivityIndicator
delay={500}
className="my-5"
label="Loading backups..."
/>
);
}
if (error) {
throw error;
}
const { backups } = data.app;
return (
<>
<Box className="flex flex-row place-content-between border-b-1 py-2">
<Text className="w-drop text-xs font-bold">Backup</Text>
<Text className="w-drop text-xs font-bold">Size</Text>
<Text className="w-drop text-xs font-bold">Backed Up</Text>
<div className="w-20" />
</Box>
<Box className="border-b-1">
{backups.length === 0 ? (
<div className="flex flex-row px-1 py-4">
<Text className="text-xs">No backups yet.</Text>
</div>
) : (
<div className="divide divide-y-1">
{backups.map((backup) => (
<BackupRow key={backup.id} backup={backup} />
))}
</div>
)}
</Box>
</>
);
}
function SectionContainer({ title }: any) {
return (
<div className="mt-6 w-full">
<Text className="text-lg font-medium">{title}</Text>
<Text className="font-normal">
The database backup includes database schema, database data and Hasura
metadata. It does not include the actual files in Storage.
</Text>
<div className="mt-6">
<BackupsTable />
</div>
</div>
);
}
function BackupsContent() {
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -159,13 +22,43 @@ function BackupsContent() {
);
}
return <SectionContainer title="Database" />;
return (
<div className="mt-6 grid w-full grid-flow-row gap-6">
<div>
<Text className="font-medium">Database</Text>
<Text color="secondary">
The database backup includes database schema, database data and Hasura
metadata. It does not include the actual files in Storage.
</Text>
</div>
<BackupList />
</div>
);
}
export default function BackupsPage() {
const { currentProject, loading } = useCurrentWorkspaceAndProject();
const { plan } = currentProject;
if (loading) {
return <ActivityIndicator label="Loading project..." delay={1000} />;
}
return (
<Container className="max-w-2.5xl">
<BackupsHeader />
<div className="grid grid-flow-col justify-between gap-2">
<Text className="text-2xl font-medium" variant="h1">
Backups
</Text>
<Chip
color={plan.isFree ? 'default' : 'success'}
label={plan.isFree ? 'Off' : 'Live'}
size="small"
/>
</div>
<RetryableErrorBoundary>
<BackupsContent />
</RetryableErrorBoundary>

View File

@@ -6,10 +6,7 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { Option } from '@/components/ui/v2/Option';
import { Radio } from '@/components/ui/v2/Radio';
import { RadioGroup } from '@/components/ui/v2/RadioGroup';
@@ -17,18 +14,12 @@ import { Select } from '@/components/ui/v2/Select';
import type { TextProps } from '@/components/ui/v2/Text';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import features from '@/data/features.json';
import { generateRandomDatabasePassword } from '@/features/database/common/utils/generateRandomDatabasePassword';
import { resetDatabasePasswordValidationSchema } from '@/features/database/settings/utils/resetDatabasePasswordValidationSchema';
import { planDescriptions } from '@/features/projects/common/utils/planDescriptions';
import { BillingPaymentMethodForm } from '@/features/projects/workspaces/components/BillingPaymentMethodForm';
import { useSubmitState } from '@/hooks/useSubmitState';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { getErrorMessage } from '@/utils/getErrorMessage';
import { getCurrentEnvironment } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import type {
PrefetchNewAppPlansFragment,
PrefetchNewAppRegionsFragment,
@@ -73,7 +64,6 @@ export function NewProjectPageContent({
// form
const [name, setName] = useState('');
const [passwordError, setPasswordError] = useState('');
const [selectedWorkspace, setSelectedWorkspace] = useState({
id: preSelectedWorkspace.id,
@@ -89,10 +79,6 @@ export function NewProjectPageContent({
code: preSelectedRegion.country.code,
});
const [databasePassword, setDatabasePassword] = useState(
generateRandomDatabasePassword(),
);
// find the first acceptable plan as default plan
const defaultSelectedPlan = plans.find((plan) => {
if (!plan.isFree) {
@@ -131,18 +117,6 @@ export function NewProjectPageContent({
(availableWorkspace) => availableWorkspace.id === selectedWorkspace.id,
);
const isK8SPostgresEnabledInCurrentEnvironment = features[
'k8s-postgres'
].enabled.find((e) => e === getCurrentEnvironment());
// function handlers
const handleGenerateRandomPassword = () => {
const newRandomDatabasePassword = generateRandomDatabasePassword();
setPasswordError('');
triggerToast('New random database password generated.');
setDatabasePassword(newRandomDatabasePassword);
};
async function handleCreateProject(event: FormEvent) {
event.preventDefault();
@@ -159,19 +133,6 @@ export function NewProjectPageContent({
return;
}
if (isK8SPostgresEnabledInCurrentEnvironment) {
try {
await resetDatabasePasswordValidationSchema.validate({
databasePassword,
});
} catch (validationError) {
setSubmitState({
error: Error(validationError.errors),
loading: false,
});
}
}
const slug = slugify(name, { lower: true, strict: true });
try {
@@ -184,9 +145,6 @@ export function NewProjectPageContent({
planId: plan.id,
workspaceId: selectedWorkspace.id,
regionId: selectedRegion.id,
postgresPassword: isK8SPostgresEnabledInCurrentEnvironment
? databasePassword
: undefined,
},
},
}),
@@ -343,86 +301,6 @@ export function NewProjectPageContent({
))}
</Select>
{isK8SPostgresEnabledInCurrentEnvironment && (
<Input
name="databasePassword"
id="databasePassword"
autoComplete="new-password"
label="Database Password"
value={databasePassword}
variant="inline"
type="password"
error={!!passwordError}
hideEmptyHelperText
endAdornment={
<InputAdornment position="end" className="mr-2">
<IconButton
color="secondary"
onClick={() => {
copy(databasePassword, 'Postgres password');
}}
variant="borderless"
aria-label="Copy password"
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</InputAdornment>
}
slotProps={{
// Note: this is supposed to fix a `validateDOMNesting` error
helperText: { component: 'div' },
}}
helperText={
<div className="grid max-w-xs grid-flow-row gap-2">
{passwordError && (
<Text
variant="subtitle2"
sx={{
color: (theme) =>
`${theme.palette.error.main} !important`,
}}
>
{passwordError}
</Text>
)}
<Box className="font-medium">
The root Postgres password for your database - it must be
strong and hard to guess.{' '}
<Button
type="button"
variant="borderless"
color="secondary"
onClick={handleGenerateRandomPassword}
className="px-1 py-0.5 text-xs underline underline-offset-2 hover:underline"
tabIndex={-1}
>
Generate a password
</Button>
</Box>
</div>
}
onChange={async (e) => {
e.preventDefault();
setSubmitState({
error: null,
loading: false,
});
setDatabasePassword(e.target.value);
try {
await resetDatabasePasswordValidationSchema.validate({
databasePassword: e.target.value,
});
setPasswordError('');
} catch (validationError) {
setPasswordError(validationError.message);
}
}}
fullWidth
/>
)}
<Select
id="region"
label="Region"
@@ -499,7 +377,7 @@ export function NewProjectPageContent({
<div className="grid w-full grid-cols-8 gap-x-4 gap-y-2">
<div className="col-span-8 sm:col-span-2">
<Text className="text-xs font-medium">Plan</Text>
<Text variant="subtitle2">You can change this later.</Text>
<Text variant="subtitle2">You can change this later</Text>
</div>
<RadioGroup
@@ -584,7 +462,7 @@ export function NewProjectPageContent({
<Button
type="submit"
loading={submitState.loading}
disabled={!!passwordError || maintenanceActive}
disabled={maintenanceActive}
id="create-app"
>
Create Project

View File

@@ -1,5 +1,6 @@
import type {
AppStateHistoryFragment,
BackupFragment,
DeploymentRowFragment,
EnvironmentVariableFragment,
PermissionVariableFragment,
@@ -39,6 +40,7 @@ export type ApplicationState = AppStateHistoryFragment;
export type Deployment = DeploymentRowFragment;
export type Workspace = WorkspaceFragment;
export type Project = ProjectFragment;
export type Backup = BackupFragment;
export interface PermissionVariable extends PermissionVariableFragment {
isSystemVariable?: boolean;

View File

@@ -1746,7 +1746,6 @@ export type ConfigSystemConfigPostgres = {
connectionString: ConfigSystemConfigPostgresConnectionString;
database: Scalars['String'];
enabled?: Maybe<Scalars['Boolean']>;
password: Scalars['String'];
};
export type ConfigSystemConfigPostgresComparisonExp = {
@@ -1756,7 +1755,6 @@ export type ConfigSystemConfigPostgresComparisonExp = {
connectionString?: InputMaybe<ConfigSystemConfigPostgresConnectionStringComparisonExp>;
database?: InputMaybe<ConfigStringComparisonExp>;
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
password?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigSystemConfigPostgresConnectionString = {
@@ -1795,14 +1793,12 @@ export type ConfigSystemConfigPostgresInsertInput = {
connectionString: ConfigSystemConfigPostgresConnectionStringInsertInput;
database: Scalars['String'];
enabled?: InputMaybe<Scalars['Boolean']>;
password: Scalars['String'];
};
export type ConfigSystemConfigPostgresUpdateInput = {
connectionString?: InputMaybe<ConfigSystemConfigPostgresConnectionStringUpdateInput>;
database?: InputMaybe<Scalars['String']>;
enabled?: InputMaybe<Scalars['Boolean']>;
password?: InputMaybe<Scalars['String']>;
};
export type ConfigSystemConfigUpdateInput = {
@@ -2549,7 +2545,6 @@ export type Apps = {
/** An object relationship */
plan: Plans;
planId: Scalars['uuid'];
postgresPassword: Scalars['String'];
providersUpdated?: Maybe<Scalars['Boolean']>;
/** An object relationship */
region: Regions;
@@ -2787,7 +2782,6 @@ export type Apps_Bool_Exp = {
paused?: InputMaybe<Boolean_Comparison_Exp>;
plan?: InputMaybe<Plans_Bool_Exp>;
planId?: InputMaybe<Uuid_Comparison_Exp>;
postgresPassword?: InputMaybe<String_Comparison_Exp>;
providersUpdated?: InputMaybe<Boolean_Comparison_Exp>;
region?: InputMaybe<Regions_Bool_Exp>;
regionId?: InputMaybe<Uuid_Comparison_Exp>;
@@ -2859,7 +2853,6 @@ export type Apps_Insert_Input = {
paused?: InputMaybe<Scalars['Boolean']>;
plan?: InputMaybe<Plans_Obj_Rel_Insert_Input>;
planId?: InputMaybe<Scalars['uuid']>;
postgresPassword?: InputMaybe<Scalars['String']>;
providersUpdated?: InputMaybe<Scalars['Boolean']>;
region?: InputMaybe<Regions_Obj_Rel_Insert_Input>;
regionId?: InputMaybe<Scalars['uuid']>;
@@ -2886,7 +2879,6 @@ export type Apps_Max_Fields = {
name?: Maybe<Scalars['String']>;
nhostBaseFolder?: Maybe<Scalars['String']>;
planId?: Maybe<Scalars['uuid']>;
postgresPassword?: Maybe<Scalars['String']>;
regionId?: Maybe<Scalars['uuid']>;
repositoryProductionBranch?: Maybe<Scalars['String']>;
slug?: Maybe<Scalars['String']>;
@@ -2909,7 +2901,6 @@ export type Apps_Max_Order_By = {
name?: InputMaybe<Order_By>;
nhostBaseFolder?: InputMaybe<Order_By>;
planId?: InputMaybe<Order_By>;
postgresPassword?: InputMaybe<Order_By>;
regionId?: InputMaybe<Order_By>;
repositoryProductionBranch?: InputMaybe<Order_By>;
slug?: InputMaybe<Order_By>;
@@ -2933,7 +2924,6 @@ export type Apps_Min_Fields = {
name?: Maybe<Scalars['String']>;
nhostBaseFolder?: Maybe<Scalars['String']>;
planId?: Maybe<Scalars['uuid']>;
postgresPassword?: Maybe<Scalars['String']>;
regionId?: Maybe<Scalars['uuid']>;
repositoryProductionBranch?: Maybe<Scalars['String']>;
slug?: Maybe<Scalars['String']>;
@@ -2956,7 +2946,6 @@ export type Apps_Min_Order_By = {
name?: InputMaybe<Order_By>;
nhostBaseFolder?: InputMaybe<Order_By>;
planId?: InputMaybe<Order_By>;
postgresPassword?: InputMaybe<Order_By>;
regionId?: InputMaybe<Order_By>;
repositoryProductionBranch?: InputMaybe<Order_By>;
slug?: InputMaybe<Order_By>;
@@ -3017,7 +3006,6 @@ export type Apps_Order_By = {
paused?: InputMaybe<Order_By>;
plan?: InputMaybe<Plans_Order_By>;
planId?: InputMaybe<Order_By>;
postgresPassword?: InputMaybe<Order_By>;
providersUpdated?: InputMaybe<Order_By>;
region?: InputMaybe<Regions_Order_By>;
regionId?: InputMaybe<Order_By>;
@@ -3073,8 +3061,6 @@ export enum Apps_Select_Column {
/** column name */
PlanId = 'planId',
/** column name */
PostgresPassword = 'postgresPassword',
/** column name */
ProvidersUpdated = 'providersUpdated',
/** column name */
RegionId = 'regionId',
@@ -3134,7 +3120,6 @@ export type Apps_Set_Input = {
/** whether or not this app is paused */
paused?: InputMaybe<Scalars['Boolean']>;
planId?: InputMaybe<Scalars['uuid']>;
postgresPassword?: InputMaybe<Scalars['String']>;
providersUpdated?: InputMaybe<Scalars['Boolean']>;
regionId?: InputMaybe<Scalars['uuid']>;
repositoryProductionBranch?: InputMaybe<Scalars['String']>;
@@ -3204,7 +3189,6 @@ export type Apps_Stream_Cursor_Value_Input = {
/** whether or not this app is paused */
paused?: InputMaybe<Scalars['Boolean']>;
planId?: InputMaybe<Scalars['uuid']>;
postgresPassword?: InputMaybe<Scalars['String']>;
providersUpdated?: InputMaybe<Scalars['Boolean']>;
regionId?: InputMaybe<Scalars['uuid']>;
repositoryProductionBranch?: InputMaybe<Scalars['String']>;
@@ -3259,8 +3243,6 @@ export enum Apps_Update_Column {
/** column name */
PlanId = 'planId',
/** column name */
PostgresPassword = 'postgresPassword',
/** column name */
ProvidersUpdated = 'providersUpdated',
/** column name */
RegionId = 'regionId',
@@ -19053,6 +19035,14 @@ export type GetPostgresSettingsQueryVariables = Exact<{
export type GetPostgresSettingsQuery = { __typename?: 'query_root', systemConfig?: { __typename?: 'ConfigSystemConfig', postgres: { __typename?: 'ConfigSystemConfigPostgres', database: string } } | null, config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', version?: string | null } | null } | null };
export type ResetDatabasePasswordMutationVariables = Exact<{
appId: Scalars['String'];
newPassword: Scalars['String'];
}>;
export type ResetDatabasePasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean };
export type GetHasuraSettingsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
@@ -19060,6 +19050,24 @@ export type GetHasuraSettingsQueryVariables = Exact<{
export type GetHasuraSettingsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', version?: string | null, settings?: { __typename?: 'ConfigHasuraSettings', enableAllowList?: boolean | null, enableRemoteSchemaPermissions?: boolean | null, enableConsole?: boolean | null, devMode?: boolean | null, corsDomain?: Array<any> | null, enabledAPIs?: Array<any> | null } | null, logs?: { __typename?: 'ConfigHasuraLogs', level?: string | null } | null, events?: { __typename?: 'ConfigHasuraEvents', httpPoolSize?: any | null } | null } } | null };
export type BackupFragment = { __typename?: 'backups', id: any, size: any, createdAt: any, completedAt?: any | null };
export type GetApplicationBackupsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type GetApplicationBackupsQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', backups: Array<{ __typename?: 'backups', id: any, size: any, createdAt: any, completedAt?: any | null }> } | null };
export type GetBackupPresignedUrlQueryVariables = Exact<{
appId: Scalars['String'];
backupId: Scalars['String'];
expireInMinutes?: InputMaybe<Scalars['Int']>;
}>;
export type GetBackupPresignedUrlQuery = { __typename?: 'query_root', getBackupPresignedUrl: { __typename?: 'BackupPresignedURL', url: string, expiresAt: any } };
export type ServiceResourcesFragment = { __typename?: 'ConfigConfig', auth?: { __typename?: 'ConfigAuth', resources?: { __typename?: 'ConfigResources', replicas: any, compute: { __typename?: 'ConfigResourcesCompute', cpu: any, memory: any } } | null } | null, hasura: { __typename?: 'ConfigHasura', resources?: { __typename?: 'ConfigResources', replicas: any, compute: { __typename?: 'ConfigResourcesCompute', cpu: any, memory: any } } | null }, postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigResources', replicas: any, compute: { __typename?: 'ConfigResourcesCompute', cpu: any, memory: any } } | null } | null, storage?: { __typename?: 'ConfigStorage', resources?: { __typename?: 'ConfigResources', replicas: any, compute: { __typename?: 'ConfigResourcesCompute', cpu: any, memory: any } } | null } | null };
export type GetResourcesQueryVariables = Exact<{
@@ -19100,13 +19108,6 @@ export type GetAppPlanAndGlobalPlansQueryVariables = Exact<{
export type GetAppPlanAndGlobalPlansQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, subdomain: string, workspace: { __typename?: 'workspaces', id: any, paymentMethods: Array<{ __typename?: 'paymentMethods', id: any }> }, plan: { __typename?: 'plans', id: any, name: string } }>, plans: Array<{ __typename?: 'plans', id: any, name: string, isFree: boolean, price: number, featureMaxDbSize: number }> };
export type GetApplicationBackupsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type GetApplicationBackupsQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', backups: Array<{ __typename?: 'backups', id: any, size: any, createdAt: any, completedAt?: any | null }> } | null };
export type GetApplicationPlanQueryVariables = Exact<{
workspace: Scalars['String'];
slug: Scalars['String'];
@@ -19265,14 +19266,6 @@ export type GetCountriesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCountriesQuery = { __typename?: 'query_root', countries: Array<{ __typename?: 'countries', code: any, name: string }> };
export type ResetPostgresPasswordMutationVariables = Exact<{
appID: Scalars['String'];
newPassword: Scalars['String'];
}>;
export type ResetPostgresPasswordMutation = { __typename?: 'mutation_root', resetPostgresPassword: boolean };
export type DeploymentRowFragment = { __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null };
export type ScheduledOrPendingDeploymentsSubSubscriptionVariables = Exact<{
@@ -19586,6 +19579,14 @@ export type UpdateWorkspaceMutationVariables = Exact<{
export type UpdateWorkspaceMutation = { __typename?: 'mutation_root', updateWorkspace?: { __typename?: 'workspaces', id: any, name: string, email: string, companyName: string, addressLine1: string, addressLine2: string, addressPostalCode: string, addressCity: string, addressCountryCode?: string | null, slug: string, taxIdType: string, taxIdValue: string } | null };
export const BackupFragmentDoc = gql`
fragment Backup on backups {
id
size
createdAt
completedAt
}
`;
export const ServiceResourcesFragmentDoc = gql`
fragment ServiceResources on ConfigConfig {
auth {
@@ -20109,6 +20110,38 @@ export type GetPostgresSettingsQueryResult = Apollo.QueryResult<GetPostgresSetti
export function refetchGetPostgresSettingsQuery(variables: GetPostgresSettingsQueryVariables) {
return { query: GetPostgresSettingsDocument, variables: variables }
}
export const ResetDatabasePasswordDocument = gql`
mutation ResetDatabasePassword($appId: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appId, newPassword: $newPassword)
}
`;
export type ResetDatabasePasswordMutationFn = Apollo.MutationFunction<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
/**
* __useResetDatabasePasswordMutation__
*
* To run a mutation, you first call `useResetDatabasePasswordMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useResetDatabasePasswordMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [resetDatabasePasswordMutation, { data, loading, error }] = useResetDatabasePasswordMutation({
* variables: {
* appId: // value for 'appId'
* newPassword: // value for 'newPassword'
* },
* });
*/
export function useResetDatabasePasswordMutation(baseOptions?: Apollo.MutationHookOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>(ResetDatabasePasswordDocument, options);
}
export type ResetDatabasePasswordMutationHookResult = ReturnType<typeof useResetDatabasePasswordMutation>;
export type ResetDatabasePasswordMutationResult = Apollo.MutationResult<ResetDatabasePasswordMutation>;
export type ResetDatabasePasswordMutationOptions = Apollo.BaseMutationOptions<ResetDatabasePasswordMutation, ResetDatabasePasswordMutationVariables>;
export const GetHasuraSettingsDocument = gql`
query GetHasuraSettings($appId: uuid!) {
config(appID: $appId, resolve: true) {
@@ -20165,6 +20198,91 @@ export type GetHasuraSettingsQueryResult = Apollo.QueryResult<GetHasuraSettingsQ
export function refetchGetHasuraSettingsQuery(variables: GetHasuraSettingsQueryVariables) {
return { query: GetHasuraSettingsDocument, variables: variables }
}
export const GetApplicationBackupsDocument = gql`
query getApplicationBackups($appId: uuid!) {
app(id: $appId) {
backups(order_by: {createdAt: desc}) {
...Backup
}
}
}
${BackupFragmentDoc}`;
/**
* __useGetApplicationBackupsQuery__
*
* To run a query within a React component, call `useGetApplicationBackupsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetApplicationBackupsQuery` 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 } = useGetApplicationBackupsQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetApplicationBackupsQuery(baseOptions: Apollo.QueryHookOptions<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>(GetApplicationBackupsDocument, options);
}
export function useGetApplicationBackupsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>(GetApplicationBackupsDocument, options);
}
export type GetApplicationBackupsQueryHookResult = ReturnType<typeof useGetApplicationBackupsQuery>;
export type GetApplicationBackupsLazyQueryHookResult = ReturnType<typeof useGetApplicationBackupsLazyQuery>;
export type GetApplicationBackupsQueryResult = Apollo.QueryResult<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>;
export function refetchGetApplicationBackupsQuery(variables: GetApplicationBackupsQueryVariables) {
return { query: GetApplicationBackupsDocument, variables: variables }
}
export const GetBackupPresignedUrlDocument = gql`
query GetBackupPresignedUrl($appId: String!, $backupId: String!, $expireInMinutes: Int) {
getBackupPresignedUrl: getBackupPresignedURL(
appID: $appId
backupID: $backupId
expireInMinutes: $expireInMinutes
) {
url
expiresAt: expires_at
}
}
`;
/**
* __useGetBackupPresignedUrlQuery__
*
* To run a query within a React component, call `useGetBackupPresignedUrlQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBackupPresignedUrlQuery` 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 } = useGetBackupPresignedUrlQuery({
* variables: {
* appId: // value for 'appId'
* backupId: // value for 'backupId'
* expireInMinutes: // value for 'expireInMinutes'
* },
* });
*/
export function useGetBackupPresignedUrlQuery(baseOptions: Apollo.QueryHookOptions<GetBackupPresignedUrlQuery, GetBackupPresignedUrlQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetBackupPresignedUrlQuery, GetBackupPresignedUrlQueryVariables>(GetBackupPresignedUrlDocument, options);
}
export function useGetBackupPresignedUrlLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBackupPresignedUrlQuery, GetBackupPresignedUrlQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetBackupPresignedUrlQuery, GetBackupPresignedUrlQueryVariables>(GetBackupPresignedUrlDocument, options);
}
export type GetBackupPresignedUrlQueryHookResult = ReturnType<typeof useGetBackupPresignedUrlQuery>;
export type GetBackupPresignedUrlLazyQueryHookResult = ReturnType<typeof useGetBackupPresignedUrlLazyQuery>;
export type GetBackupPresignedUrlQueryResult = Apollo.QueryResult<GetBackupPresignedUrlQuery, GetBackupPresignedUrlQueryVariables>;
export function refetchGetBackupPresignedUrlQuery(variables: GetBackupPresignedUrlQueryVariables) {
return { query: GetBackupPresignedUrlDocument, variables: variables }
}
export const GetResourcesDocument = gql`
query GetResources($appId: uuid!) {
config(appID: $appId, resolve: true) {
@@ -20358,49 +20476,6 @@ export type GetAppPlanAndGlobalPlansQueryResult = Apollo.QueryResult<GetAppPlanA
export function refetchGetAppPlanAndGlobalPlansQuery(variables: GetAppPlanAndGlobalPlansQueryVariables) {
return { query: GetAppPlanAndGlobalPlansDocument, variables: variables }
}
export const GetApplicationBackupsDocument = gql`
query getApplicationBackups($appId: uuid!) {
app(id: $appId) {
backups(order_by: {createdAt: desc}) {
id
size
createdAt
completedAt
}
}
}
`;
/**
* __useGetApplicationBackupsQuery__
*
* To run a query within a React component, call `useGetApplicationBackupsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetApplicationBackupsQuery` 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 } = useGetApplicationBackupsQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetApplicationBackupsQuery(baseOptions: Apollo.QueryHookOptions<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>(GetApplicationBackupsDocument, options);
}
export function useGetApplicationBackupsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>(GetApplicationBackupsDocument, options);
}
export type GetApplicationBackupsQueryHookResult = ReturnType<typeof useGetApplicationBackupsQuery>;
export type GetApplicationBackupsLazyQueryHookResult = ReturnType<typeof useGetApplicationBackupsLazyQuery>;
export type GetApplicationBackupsQueryResult = Apollo.QueryResult<GetApplicationBackupsQuery, GetApplicationBackupsQueryVariables>;
export function refetchGetApplicationBackupsQuery(variables: GetApplicationBackupsQueryVariables) {
return { query: GetApplicationBackupsDocument, variables: variables }
}
export const GetApplicationPlanDocument = gql`
query getApplicationPlan($workspace: String!, $slug: String!) {
apps(where: {workspace: {slug: {_eq: $workspace}}, slug: {_eq: $slug}}) {
@@ -21353,38 +21428,6 @@ export type GetCountriesQueryResult = Apollo.QueryResult<GetCountriesQuery, GetC
export function refetchGetCountriesQuery(variables?: GetCountriesQueryVariables) {
return { query: GetCountriesDocument, variables: variables }
}
export const ResetPostgresPasswordDocument = gql`
mutation resetPostgresPassword($appID: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appID, newPassword: $newPassword)
}
`;
export type ResetPostgresPasswordMutationFn = Apollo.MutationFunction<ResetPostgresPasswordMutation, ResetPostgresPasswordMutationVariables>;
/**
* __useResetPostgresPasswordMutation__
*
* To run a mutation, you first call `useResetPostgresPasswordMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useResetPostgresPasswordMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [resetPostgresPasswordMutation, { data, loading, error }] = useResetPostgresPasswordMutation({
* variables: {
* appID: // value for 'appID'
* newPassword: // value for 'newPassword'
* },
* });
*/
export function useResetPostgresPasswordMutation(baseOptions?: Apollo.MutationHookOptions<ResetPostgresPasswordMutation, ResetPostgresPasswordMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ResetPostgresPasswordMutation, ResetPostgresPasswordMutationVariables>(ResetPostgresPasswordDocument, options);
}
export type ResetPostgresPasswordMutationHookResult = ReturnType<typeof useResetPostgresPasswordMutation>;
export type ResetPostgresPasswordMutationResult = Apollo.MutationResult<ResetPostgresPasswordMutation>;
export type ResetPostgresPasswordMutationOptions = Apollo.BaseMutationOptions<ResetPostgresPasswordMutation, ResetPostgresPasswordMutationVariables>;
export const ScheduledOrPendingDeploymentsSubDocument = gql`
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(

View File

@@ -1,9 +1,13 @@
import { triggerToast } from '@/utils/toast';
export default function copy(toCopy: string, name: string) {
export default function copy(toCopy: string, name?: string) {
navigator.clipboard.writeText(toCopy).catch(() => {
triggerToast('Error while copying');
});
if (!name) {
return;
}
triggerToast(`${name} copied to clipboard`);
}

View File

@@ -1,4 +1,3 @@
import features from '@/data/features.json';
import { ApplicationStatus } from '@/types/application';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import slugify from 'slugify';
@@ -73,7 +72,3 @@ export function getRelativeDateByApplicationState(date: string) {
return Math.floor(difference / 1000);
}
export const isK8SPostgresEnabledInCurrentEnvironment = features[
'k8s-postgres'
].enabled.find((e) => e === getCurrentEnvironment());

View File

@@ -4,7 +4,7 @@ export default function getColor() {
const colorPreference =
typeof window !== 'undefined'
? window.localStorage.getItem(COLOR_PREFERENCE_STORAGE_KEY)
: 'light';
: 'system';
const prefersDarkMode =
typeof window !== 'undefined'
? window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@@ -2,17 +2,18 @@ import { Box } from '@/components/ui/v2/Box';
import { createTheme } from '@/components/ui/v2/createTheme';
import { ThemeProvider } from '@mui/material';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { toast } from 'react-hot-toast';
import getColor from './getColor';
export default function triggerToast(text: string) {
export default function triggerToast(text: ReactNode) {
const color = getColor();
toast.custom((t) => (
<ThemeProvider theme={createTheme(color)}>
<Box
className={clsx(
'rounded-sm+ px-2 py-1.5 font-normal shadow-md',
'rounded-sm+ px-2 py-1.5 text-center font-normal shadow-md',
t.visible ? 'animate-enter' : 'animate-leave',
)}
sx={{

View File

@@ -0,0 +1,77 @@
---
title: 'Configuration Overlays'
sidebar_label: 'Configuration Overlays'
sidebar_position: 4
---
## Introduction
Nhost uses a single configuration file to configure both cloud projects and the local development environment, ensuring a near-identical replica of the production environment. This approach also simplifies connecting a repository to multiple projects, keeping them in sync with minimal effort. However, in cases where minor differences need to be accommodated, Nhost offers overlays - files containing operations to modify the configuration file:
```
[
{
"op": "replace",
"path": "/auth/method/emailPassword/emailVerificationRequired",
"value": false
},
{
"op": "replace",
"path": "/auth/redirections/clientUrl",
"value": "https://staging.myapp.io"
},
{
"op": "add",
"path": "/global/environment/3",
"value": {
"name": "ENVIRONMENT",
"value": "staging"
}
}
]
```
The overlay above will perform the following changes to the configuration file:
1. Disable email verification
2. Set the correct client URL for the staging environment
3. Add the environment variable `ENVIRONMENT=staging`
:::info
Overlays are based on [RFC6902](https://www.rfc-editor.org/rfc/rfc6902)
:::
It's important to note that overlays in Nhost do not modify the original files, but rather manipulate the resulting configuration at runtime. This allows multiple environments to safely share the same base configuration and rely on the overlay to accommodate differences. Additionally, changes to the base configuration file will propagate to all environments unless an overlay operation prevents it, making overlays a convenient and efficient way to manage configuration differences.
## Creating Overlays
You can create overlays with the CLI command `nhost config edit [--subdomain $SUBDOMAIN]`:
![open editor](/img/overlays/1.png)
:::info
If you need to create an overlay for your local development environment remember to use `local` as subdomain
:::
This will open an editor with the configuration for your `$SUBDOMAIN`:
![editor](/img/overlays/2.png)
You can perform as many changes as needed using overlays. For example, you can add an environment variable, enable Hasura remote permissions, and change the client URL.
![editor](/img/overlays/3.png)
You can verify the overlay by checking the file `nhost/overlays/$SUBDOMAIN.json`
![verify overlay](/img/overlays/4.png)
## Viewing Configuration
To verify that your final configuration is correct, you can use the command `nhost config show [--subdomain $SUBDOMAIN]`. This command will apply the specified overlay (if it exists) and render the configuration using the local secrets.
![view configuration](/img/overlays/5.png)
## Validating Configuration
Finally, you can validate your configuration overlay with the command `nhost config validate [--subdomain $SUBDOMAIN]`
![view configuration](/img/overlays/6.png)

View File

@@ -1,108 +0,0 @@
---
title: 'Patching Local Environment'
sidebar_label: 'Patching Local Environment'
sidebar_position: 4
---
In some cases you may need to modify your configuration file to accommodate minor deviations from your production environment. For instance, imagine your `nhost.toml` file looks like:
```
[global]
[[global.environment]]
name = 'ENVIRONMENT'
value = 'production'
... (omitted for brevity)
[hasura]
version = 'v2.24.1-ce'
... (omitted for brevity)
[hasura.logs]
level = 'warn'
... (omitted for brevity)
[auth.redirections]
clientUrl = 'https://my.app.com'
... (omitted for brevity)
[auth.method.oauth]
[auth.method.oauth.apple]
enabled = true
clientId = '{{ secrets.APPLE_CLIENT_ID }}'
keyId = '{{ secrets.APPLE_KEY_ID }}'
teamId = '{{ secrets.APPLE_TEAM_ID }}'
privateKey = '{{ secrets.APPLE_PRIVATE_KEY }}'
... (omitted for brevity)
```
While this may work in production you may want to do minor tweaks in your local development. To do so we rely on [JSON patches RFC6902](https://datatracker.ietf.org/doc/html/rfc6902). To make use of it just drop a file under `nhost/overlays/local.yaml`. For instance, a file with the following contents would apply minor modifications to your local environment without affecting production:
```
- op: replace
path: /hasura/version # override hasura version
value: "v2.25.1-ce"
- op: replace
path: /global/environment/0 # replace first environment variables
value:
name: ENVIRONMENT
value: development
- op: add # add a new env var
path: /global/environment/-
value:
name: FUNCTION_LOG_LEVEL
value: debug
- op: replace # change the client url to local
path: /auth/redirections/clientUrl
value: http://localhost:3000
- op: remove # remove apple authentication
path: /auth/method/oauth/apple
```
To verify that the file is being manipulated as we desire we can use the `nhost` cli:
```
$ nhost config show
[global]
[[global.environment]]
name = 'ENVIRONMENT'
value = 'development'
[[global.environment]]
name = 'FUNCTION_LOG_LEVEL'
value = 'debug'
... (omitted for brevity)
[hasura]
version = 'v2.25.1-ce'
... (omitted for brevity)
[auth.redirections]
clientUrl = 'http://localhost:3000'
... (omitted for brevity)
[auth.method.oauth]
[auth.method.oauth.apple]
enabled = false
... (omitted for brevity)
```
Once you have finished making changes, don't forget to restart your development environment by running the command `nhost up` after modifying your configuration.
:::info
While it may be convenient to modify your local environment for development, the further it deviates from production, the harder it is to detect issues before release. Therefore, we recommend keeping changes strictly necessary
:::

View File

@@ -41,6 +41,11 @@ Once you've tested your changes in the staging environment, you can create a new
This will automatically trigger a new deployment to the production project on the Nhost platform.
## Configuration Overlays
While Nhost uses a single file to deploy all of the environments connected to the same repository and branch, overlays allow you to accommodate for minor differences in those environments by allowing you to define rules to modify the base configuration. For details on overlays head to [Configuration Overlays](/cli/overlays)
## Summary
Now you have two environments, one for staging and one for production. You can use this workflow to do local development, and test your changes in a staging environment before deploying them to production.

BIN
docs/static/img/overlays/1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

BIN
docs/static/img/overlays/2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

BIN
docs/static/img/overlays/3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

BIN
docs/static/img/overlays/4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

BIN
docs/static/img/overlays/5.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

BIN
docs/static/img/overlays/6.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

View File

@@ -8,15 +8,15 @@
outputs = { self, nixpkgs, flake-utils, nix-filter }:
flake-utils.lib.eachDefaultSystem (system:
let
version = "v0.9.7";
version = "v1.1.0";
dist = {
aarch64-darwin = rec {
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-darwin-arm64.tar.gz";
sha256 = "sha256-K0yKwsEsiR1JjsyWxxlo/hIrxzsmnwcw+KUZdT8/Rt4=";
sha256 = "sha256-tF40CEkA357yzg2Gmc9ubjHJ5FlI9qQTdVdWNY/+f+Y=";
};
x86_64-linux = rec {
url = "https://github.com/nhost/cli/releases/download/${version}/cli-${version}-linux-amd64.tar.gz";
sha256 = "0llfk6fa6l7fcp6xpfx2kg3lzlzpdlkzl9lnficiq76s341slxbc";
sha256 = "sha256-KLv06dI7A+5KGJ5F8xM1qC+oqHRmJ4kMaifLvaTFqak=";
};
};
overlays = [
@@ -124,9 +124,8 @@
default = pkgs.mkShell {
buildInputs = with pkgs; [
nhost
nodejs_16
nodejs_18
nodePackages.pnpm
nhost
] ++ buildInputs ++ nativeBuildInputs;
};
};

View File

@@ -62,7 +62,7 @@
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-c8": "^0.30.0",
"@vitest/coverage-v8": "^0.32.0",
"eslint": "^8.26.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-flowtype": "^8.0.3",
@@ -76,18 +76,18 @@
"husky": "^8.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"turbo": "1.10.1",
"turbo": "1.10.3",
"typedoc": "^0.22.18",
"typescript": "4.9.5",
"vite": "^4.3.8",
"vite-plugin-dts": "^2.3.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.30.0"
"vitest": "^0.32.0"
},
"resolutions": {
"graphql": "16.6.0"
},
"packageManager": "pnpm@8.5.1",
"packageManager": "pnpm@8.6.2",
"engines": {
"node": ">=18 <19",
"pnpm": ">=8.0.0"

1329
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff