Compare commits

..

81 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
d2d590db7e Merge pull request #2369 from nhost/changeset-release/main
chore: update versions
2023-11-24 16:37:42 +01:00
github-actions[bot]
3bdbefc015 chore: update versions 2023-11-24 13:05:27 +00:00
Hassan Ben Jobrane
79081b43c2 Merge pull request #2376 from nhost/feat/database/sql-editor
feat(dashboard): add sql editor
2023-11-24 14:03:15 +01:00
Hassan Ben Jobrane
2e2248fd44 chore: add changeset 2023-11-24 10:07:21 +01:00
Hassan Ben Jobrane
63358eb80b chore: add comments 2023-11-24 09:59:55 +01:00
Hassan Ben Jobrane
ded674fab6 fix: add min height to codemirror 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
85f2f28902 refactor(dashboard): move run-sql logic to a custom hook 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
b8e9ad831e refactor(dashboard): add proper error handling 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
4e0c5dd1d3 refactor(dashboard): improve SQL parsing in SQLEditor 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
b874109c6d fix: rely on error returned from api call to update metadata 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
21b926cc07 feat(dashboard): add create migration option to the sql editor 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
c35cd47d97 feat: implement track tables in the sql editor 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
8dcd801c7c feat(dashboard): add support for resizing the sql query results container 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
e3199be749 feat(dashboard): add sql editor tab and basic func 2023-11-24 09:55:16 +01:00
Hassan Ben Jobrane
284b31e036 Merge pull request #2383 from nhost/chore/dashboard/update-node
chore: update node to v18
2023-11-24 09:18:42 +01:00
Hassan Ben Jobrane
e7593c7de8 chore: update node to v18 in Dockerfile 2023-11-23 16:15:07 +01:00
Hassan Ben Jobrane
e6d862ac1b Merge pull request #2342 from nhost/fix/quickstarts-workspace-deps
fix: ensure `pnpm clean` and `pnpm install` work correctly for the quickstarts
2023-11-15 21:30:21 +01:00
Hassan Ben Jobrane
f73672372f chore: update baseURL in playwright.config.js 2023-11-15 21:08:03 +01:00
Hassan Ben Jobrane
7f12b98d94 chore: fix linter issue 2023-11-15 21:01:07 +01:00
Hassan Ben Jobrane
d79b66314d chore: fix linter issues 2023-11-15 20:49:11 +01:00
Hassan Ben Jobrane
2a58266592 chore: add allowedUrls to auth.redirections and set redirect option for Google sign-in 2023-11-15 20:24:53 +01:00
Hassan Ben Jobrane
44c2c5467d fix: replace @apollo/client with graphql-tag 2023-11-15 20:21:54 +01:00
Hassan Ben Jobrane
142752cb79 Revert "fix: update Apollo client import"
This reverts commit 11a46a0db1.
2023-11-15 20:13:48 +01:00
Hassan Ben Jobrane
b05236a23c chore: run pnpm install 2023-11-15 19:53:11 +01:00
Hassan Ben Jobrane
11a46a0db1 fix: update Apollo client import 2023-11-15 19:53:11 +01:00
Hassan Ben Jobrane
cedff501d6 chore: update auth version to 0.22.1 2023-11-15 19:53:11 +01:00
Hassan Ben Jobrane
7c426dafb2 fix: rectify clean scripts 2023-11-15 19:53:11 +01:00
Hassan Ben Jobrane
57e7f794f5 fix: make sure pnpm clean and pnpm install work correctly for the quickstarts 2023-11-15 19:53:11 +01:00
Hassan Ben Jobrane
d4b6cb0acf Merge pull request #2370 from nhost/chore/quickstarts/update-metadata
chore(quickstarts): add virus table metadata
2023-11-15 19:50:17 +01:00
Nuno Pato
5d0cf8814b Merge pull request #2372 from nhost/chore/dashboard-update-storage-capacity-alert
Chore/dashboard update storage capacity alert
2023-11-13 16:30:17 -01:00
Nuno Pato
96cf17bbeb Apply suggestions from code review
fix typos

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2023-11-13 16:26:16 -01:00
Nuno Pato
ed1a8d458e add changeset 2023-11-13 16:12:20 -01:00
Nuno Pato
8077495c18 Change dashboard alert for volume capacity 2023-11-13 16:09:33 -01:00
Hassan Ben Jobrane
b617ec7186 chore(quickstarts): add virus table metadata 2023-11-13 17:02:43 +01:00
Hassan Ben Jobrane
bb2da11dd4 Merge pull request #2367 from nhost/fix/docs/signin-linkedin-guide
fix(docs): add instructions for enabling Sign In with LinkedIn using OpenID Connect
2023-11-13 16:18:03 +01:00
Hassan Ben Jobrane
94fa824e7d Merge pull request #2366 from nhost/feat/react-apollo/sign-in-with-linked-in
feat(examples): add sign-in with Linked to react-apollo
2023-11-13 15:56:30 +01:00
Hassan Ben Jobrane
32d1ee124f chore(react-apollo): update auth version to 0.22.1 2023-11-13 15:29:28 +01:00
Hassan Ben Jobrane
138bf9eb5a chore: add changeset 2023-11-11 20:26:50 +01:00
Hassan Ben Jobrane
d8d9310e0b fix: add instructions for enabling Sign In with LinkedIn using OpenID Connect 2023-11-11 20:26:05 +01:00
Hassan Ben Jobrane
67b2c044b8 chore: add changeset 2023-11-11 16:14:33 +01:00
Hassan Ben Jobrane
0b7790ca83 feat(examples): add sign-in with Linked to react-apollo 2023-11-11 16:11:56 +01:00
Hassan Ben Jobrane
55267c680e Merge pull request #2358 from nhost/changeset-release/main
chore: update versions
2023-11-10 16:42:15 +01:00
github-actions[bot]
4d856f557f chore: update versions 2023-11-10 15:22:59 +00:00
Hassan Ben Jobrane
64c579cf8c Merge pull request #2365 from nhost/feat/delete-account
feat: delete account
2023-11-10 16:21:04 +01:00
Hassan Ben Jobrane
eae65c715b fix: disable delete account when user has projects 2023-11-10 15:26:38 +01:00
Hassan Ben Jobrane
9e69f9f235 Merge pull request #2362 from spakanati/feat/export-url-helpers
feat: export urlFromSubdomain helper
2023-11-10 14:30:08 +01:00
Hassan Ben Jobrane
8b127fbb62 chore: add changeset 2023-11-10 14:13:27 +01:00
Hassan Ben Jobrane
86ba2081ec chore: fix docusaurus front matter issue 2023-11-10 14:13:20 +01:00
Hassan Ben Jobrane
7c2c31082a chore: add changeset 2023-11-10 11:50:54 +01:00
Hassan Ben Jobrane
60f705b033 feat: add user account deletion functionality 2023-11-10 11:49:22 +01:00
Sheena Pakanati
ea34635eb2 feat: export urlFromSubdomain helper 2023-11-08 11:30:10 -05:00
Hassan Ben Jobrane
2004687044 Merge pull request #2360 from nhost/fix/examples/react-apollo
fix(react-apollo): update Apple OAuth secrets in nhost.toml
2023-11-07 11:24:51 +01:00
Hassan Ben Jobrane
bd025d43ca fix: update Apple OAuth secrets in nhost.toml 2023-11-07 10:54:16 +01:00
Hassan Ben Jobrane
87a05f7374 Merge pull request #2353 from nhost/feat/react-appollo/signin-with-apple
feat(react-apollo): add SignIn with Apple
2023-11-07 08:53:19 +01:00
Hassan Ben Jobrane
798f147db7 chore: remove console.log statement 2023-11-06 20:13:05 +01:00
Hassan Ben Jobrane
62b7fd2376 chore: update auth version to 0.21.4 2023-11-06 20:11:33 +01:00
David Barroso
1ee021b4a3 chore(docs): remove custom domains from roadmap (#2352) 2023-11-04 12:40:18 +01:00
Hassan Ben Jobrane
6e61dce297 chore: add changeset 2023-11-03 17:28:01 +01:00
Hassan Ben Jobrane
bd744e52dc feat(examples): add SignIn with Apple to the react-apollo example 2023-11-03 17:26:23 +01:00
Nestor Manrique
85723d740b Merge pull request #2343 from nhost/nestor/fix/ingress-tenant-dashboard
fix (observability): ingress tenant dashboard
2023-10-26 21:56:18 +02:00
Hassan Ben Jobrane
36e79e7b32 Merge pull request #2344 from nhost/chore/quickstarts/upgrade-storage
chore: bump quickstarts storage to `0.4.0`
2023-10-26 11:39:58 +01:00
Hassan Ben Jobrane
f61264b319 chore: bump quickstarts storage to 0.4.0 2023-10-26 11:22:41 +01:00
Nestor Manrique
e84d9d2576 Fix legends 2023-10-26 11:28:33 +02:00
Hassan Ben Jobrane
ea69d4f0f1 Merge pull request #2341 from nhost/changeset-release/main
chore: update versions
2023-10-25 14:53:21 +01:00
github-actions[bot]
212d58bee5 chore: update versions 2023-10-25 13:22:09 +00:00
Hassan Ben Jobrane
c3d6b7beec Merge pull request #2333 from nhost/feat/vue-sdk/upload-multiple-files
feat(vue-sdk): add support for uploading multiple files
2023-10-25 14:18:36 +01:00
Hassan Ben Jobrane
5d5d8ef4f3 chore: use @nhost/nhost-js from workspace 2023-10-25 13:31:12 +01:00
Hassan Ben Jobrane
deb61fe97c chore: add @nhost/nhost-js to vue-apollo example 2023-10-25 13:21:36 +01:00
Nestor Manrique
04d36154b0 Merge pull request #2334 from nhost/nestor/feat/add-ingress-dashboard
feat(observability): Add ingress metrics dashboard for tenants
2023-10-25 14:21:01 +02:00
Hassan Ben Jobrane
203cfb10b9 chore: fix JSDoc 2023-10-25 12:43:54 +01:00
Hassan Ben Jobrane
9690f871fa chore: fix JSDoc 2023-10-25 11:44:45 +01:00
Hassan Ben Jobrane
74a6b93971 Merge pull request #2335 from nhost/chore/examples/upgrade-to-node18
chore: update toml files to use node 18
2023-10-25 10:36:37 +01:00
Nestor Manrique
dd4c0d2430 wip 2023-10-25 03:30:52 +02:00
Hassan Ben Jobrane
83f2ca5cde chore: update toml files to use node 18 2023-10-24 16:39:09 +01:00
Hassan Ben Jobrane
0c49e757c8 chore: add changeset 2023-10-24 16:25:07 +01:00
Hassan Ben Jobrane
e90a9d7696 feat: add storage page to vue-apollo example 2023-10-24 16:20:02 +01:00
Hassan Ben Jobrane
00a06466f5 fix: return refs from useFileUpload 2023-10-24 16:20:02 +01:00
Hassan Ben Jobrane
8ca9f76cb2 wip: add support for uploading multiple files 2023-10-24 16:20:02 +01:00
Hassan Ben Jobrane
78113dd62a wip: feat: vue-sdk: introduce new composable to upload multiple files 2023-10-24 16:20:02 +01:00
Nestor Manrique
adb0ee82c6 wip 2023-10-24 14:29:25 +02:00
Nestor Manrique
a41bb6cae6 wip 2023-10-24 14:05:42 +02:00
95 changed files with 6127 additions and 664 deletions

View File

@@ -1,5 +1,20 @@
# @nhost/dashboard
## 0.21.0
### Minor Changes
- ed1a8d458: Update alert message on increasing PostgreSQL's volume capacity
- 2e2248fd4: feat(dashboard): add SQL editor
## 0.20.28
### Patch Changes
- 7c2c31082: feat: add support for users to delete their account
- @nhost/react-apollo@6.0.1
- @nhost/nextjs@1.13.40
## 0.20.27
### Patch Changes

View File

@@ -1,4 +1,4 @@
FROM node:16-alpine AS pruner
FROM node:18-alpine AS pruner
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
@@ -7,7 +7,7 @@ RUN yarn global add turbo@1.10.11
COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker
FROM node:16-alpine AS builder
FROM node:18-alpine AS builder
ARG TURBO_TOKEN
ARG TURBO_TEAM
@@ -40,7 +40,7 @@ COPY turbo.json turbo.json
COPY config/ config/
RUN pnpm build:dashboard
FROM node:16-alpine AS runner
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.20.27",
"version": "0.21.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -19,7 +19,7 @@
},
"dependencies": {
"@apollo/client": "^3.7.10",
"@codemirror/language": "^6.3.0",
"@codemirror/lang-sql": "^6.5.4",
"@emotion/cache": "^11.10.5",
"@emotion/react": "^11.10.5",
"@emotion/server": "^11.4.0",
@@ -44,6 +44,8 @@
"@tanstack/react-query": "^4.16.1",
"@tanstack/react-table": "^8.5.30",
"@tanstack/react-virtual": "^3.0.0-beta.23",
"@uiw/codemirror-theme-github": "^4.21.20",
"@uiw/react-codemirror": "^4.21.20",
"analytics-node": "^6.2.0",
"bcryptjs": "^2.4.3",
"clsx": "^1.2.1",
@@ -70,6 +72,7 @@
"react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0",
"react-merge-refs": "^1.1.0",
"react-resizable-layout": "^0.7.2",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"sharp": "^0.32.0",

View File

@@ -0,0 +1,26 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function TerminalIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="Trash"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.49851 3.43968L2.93795 2.94141L1.94141 4.06252L2.50196 4.56079L6.37134 8.00024L2.50196 11.4397L1.94141 11.938L2.93795 13.0591L3.49851 12.5608L7.99851 8.56079C8.15863 8.41847 8.25024 8.21446 8.25024 8.00024C8.25024 7.78601 8.15863 7.582 7.99851 7.43968L3.49851 3.43968ZM7.99987 11.2502H7.24987V12.7502H7.99987H13.9999H14.7499V11.2502H13.9999H7.99987Z"
fill="currentColor"
/>
</SvgIcon>
);
}
TerminalIcon.displayName = 'NhostTerminalIcon';
export default TerminalIcon;

View File

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

View File

@@ -0,0 +1,161 @@
import { useDialog } from '@/components/common/DialogProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useDeleteUserAccountMutation,
useGetAllWorkspacesAndProjectsQuery,
} from '@/utils/__generated__/graphql';
import { type ApolloError } from '@apollo/client';
import { useSignOut, useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
function ConfirmDeleteAccountModal({
close,
onDelete,
}: {
onDelete?: () => Promise<any>;
close: () => void;
}) {
const [remove, setRemove] = useState(false);
const [loadingRemove, setLoadingRemove] = useState(false);
const user = useUserData();
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
skip: !user,
});
const userHasProjects =
!loading && data?.workspaces.some((workspace) => workspace.projects.length);
const userData = useUserData();
const [deleteUserAccount] = useDeleteUserAccountMutation({
variables: { id: userData?.id },
});
const onClickConfirm = async () => {
setLoadingRemove(true);
await toast.promise(
deleteUserAccount(),
{
loading: 'Deleting your account...',
success: `The account has been deleted successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting your account. Please try again.'
);
},
},
getToastStyleProps(),
);
onDelete?.();
close();
};
return (
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
Delete Account?
</Text>
{userHasProjects && (
<Text
variant="subtitle2"
className="font-bold"
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
>
You still have active projects. Please delete your projects before
proceeding with the account deletion.
</Text>
)}
<Box className="my-4">
<Checkbox
id="accept-1"
label={`I'm sure I want to delete my account`}
className="py-2"
checked={remove}
onChange={(_event, checked) => setRemove(checked)}
aria-label="Confirm Delete Project #1"
/>
</Box>
<div className="grid grid-flow-row gap-2">
<Button
color="error"
onClick={onClickConfirm}
disabled={userHasProjects}
loading={loadingRemove}
>
Delete
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</Box>
);
}
export default function DeleteAccount() {
const router = useRouter();
const { signOut } = useSignOut();
const { openDialog, closeDialog } = useDialog();
const onDelete = async () => {
await signOut();
await router.push('/signin');
};
const confirmDeleteAccount = async () => {
openDialog({
component: (
<ConfirmDeleteAccountModal close={closeDialog} onDelete={onDelete} />
),
});
};
return (
<SettingsContainer
title="Delete Account"
description="Please proceed with caution as the removal of your Personal Account and its contents from the Nhost platform is irreversible. This action will permanently delete your account and all associated data."
className="px-0"
slotProps={{
submitButton: { className: 'hidden' },
footer: { className: 'hidden' },
}}
>
<Box className="grid grid-flow-row border-t-1">
<Button
color="error"
className="mx-4 mt-4 justify-self-end"
onClick={confirmDeleteAccount}
>
Delete Personal Account
</Button>
</Box>
</SettingsContainer>
);
}

View File

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

View File

@@ -0,0 +1,5 @@
mutation deleteUserAccount($id: uuid!) {
deleteUser(id: $id) {
__typename
}
}

View File

@@ -17,6 +17,7 @@ import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon'
import { LockIcon } from '@/components/ui/v2/icons/LockIcon';
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TerminalIcon } from '@/components/ui/v2/icons/TerminalIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { UsersIcon } from '@/components/ui/v2/icons/UsersIcon';
import { Link } from '@/components/ui/v2/Link';
@@ -86,7 +87,9 @@ function DataBrowserSidebarContent({
const isGitHubConnected = !!currentProject?.githubRepository;
const router = useRouter();
const {
asPath,
query: { workspaceSlug, appSlug, dataSourceSlug, schemaSlug, tableSlug },
} = router;
@@ -108,6 +111,8 @@ function DataBrowserSidebarContent({
*/
const [sidebarMenuTable, setSidebarMenuTable] = useState<string>();
const sqlEditorHref = `/${workspaceSlug}/${appSlug}/database/browser/default/editor`;
useEffect(() => {
if (selectedSchema) {
return;
@@ -258,194 +263,135 @@ function DataBrowserSidebarContent({
}
return (
<div className="grid gap-1">
{schemas && schemas.length > 0 && (
<Select
renderValue={(option) => (
<span className="grid grid-flow-col items-center gap-1">
{option?.label}
</span>
)}
slotProps={{
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
}}
value={selectedSchema}
onChange={(_event, value) => setSelectedSchema(value as string)}
>
{schemas.map((schema) => (
<Option
className="grid grid-flow-col items-center gap-1"
value={schema.schema_name}
key={schema.schema_name}
>
<Text className="text-sm">
<Text component="span" color="disabled">
schema.
</Text>
<Text component="span" className="font-medium">
{schema.schema_name}
</Text>
</Text>
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
<LockIcon
className="h-3 w-3"
sx={{ color: 'text.secondary' }}
/>
)}
</Option>
))}
</Select>
)}
{isGitHubConnected && (
<Box
className="mt-1.5 grid grid-flow-row justify-items-start gap-2 rounded-md p-2"
sx={{ backgroundColor: 'grey.200' }}
>
<Text>
Your project is connected to GitHub. Please use the CLI to make
schema changes.
</Text>
<Link
href="https://docs.nhost.io/platform/github-integration"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="grid grid-flow-col items-center justify-start gap-1"
<Box className="flex h-full flex-col justify-between">
<Box className="flex flex-col px-2">
{schemas && schemas.length > 0 && (
<Select
renderValue={(option) => (
<span className="grid grid-flow-col items-center gap-1">
{option?.label}
</span>
)}
slotProps={{
listbox: { className: 'max-w-[220px] min-w-[initial] w-full' },
popper: { className: 'max-w-[220px] min-w-[initial] w-full' },
}}
value={selectedSchema}
onChange={(_event, value) => setSelectedSchema(value as string)}
>
Learn More <ArrowRightIcon />
</Link>
</Box>
)}
{!isSelectedSchemaLocked && (
<Button
variant="borderless"
endIcon={<PlusIcon />}
className="mt-1 w-full justify-between px-2"
onClick={() => {
openDrawer({
title: 'Create a New Table',
component: (
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
),
});
onSidebarItemClick();
}}
disabled={isGitHubConnected}
>
New Table
</Button>
)}
{schemas && schemas.length > 0 && tablesInSelectedSchema.length === 0 && (
<Text className="py-1.5 px-2 text-xs" color="disabled">
No tables found.
</Text>
)}
<nav aria-label="Database navigation">
{tablesInSelectedSchema.length > 0 && (
<List className="grid gap-1 pb-6">
{tablesInSelectedSchema.map((table) => {
const tablePath = `${table.table_schema}.${table.table_name}`;
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
return (
<ListItem.Root
className="group"
key={tablePath}
secondaryAction={
<Dropdown.Root
id="table-management-menu"
onOpen={() => setSidebarMenuTable(tablePath)}
onClose={() => setSidebarMenuTable(undefined)}
>
<Dropdown.Trigger
asChild
hideChevron
disabled={tablePath === removableTable}
{schemas.map((schema) => (
<Option
className="grid grid-flow-col items-center gap-1"
value={schema.schema_name}
key={schema.schema_name}
>
<Text className="text-sm">
<Text component="span" color="disabled">
schema.
</Text>
<Text component="span" className="font-medium">
{schema.schema_name}
</Text>
</Text>
{(isSchemaLocked(schema.schema_name) || isGitHubConnected) && (
<LockIcon
className="h-3 w-3"
sx={{ color: 'text.secondary' }}
/>
)}
</Option>
))}
</Select>
)}
{isGitHubConnected && (
<Box
className="mt-1.5 grid grid-flow-row justify-items-start gap-2 rounded-md p-2"
sx={{ backgroundColor: 'grey.200' }}
>
<Text>
Your project is connected to GitHub. Please use the CLI to make
schema changes.
</Text>
<Link
href="https://docs.nhost.io/platform/github-integration"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="grid grid-flow-col items-center justify-start gap-1"
>
Learn More <ArrowRightIcon />
</Link>
</Box>
)}
{!isSelectedSchemaLocked && (
<Button
variant="borderless"
endIcon={<PlusIcon />}
className="mt-1 w-full justify-between px-2"
onClick={() => {
openDrawer({
title: 'Create a New Table',
component: (
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
),
});
onSidebarItemClick();
}}
disabled={isGitHubConnected}
>
New Table
</Button>
)}
{schemas && schemas.length > 0 && tablesInSelectedSchema.length === 0 && (
<Text className="py-1.5 px-2 text-xs" color="disabled">
No tables found.
</Text>
)}
<nav aria-label="Database navigation">
{tablesInSelectedSchema.length > 0 && (
<List className="grid gap-1 pb-6">
{tablesInSelectedSchema.map((table) => {
const tablePath = `${table.table_schema}.${table.table_name}`;
const isSelected = `${schemaSlug}.${tableSlug}` === tablePath;
const isSidebarMenuOpen = sidebarMenuTable === tablePath;
return (
<ListItem.Root
className="group"
key={tablePath}
secondaryAction={
<Dropdown.Root
id="table-management-menu"
onOpen={() => setSidebarMenuTable(tablePath)}
onClose={() => setSidebarMenuTable(undefined)}
>
<IconButton
variant="borderless"
color={isSelected ? 'primary' : 'secondary'}
className={twMerge(
!isSelected &&
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
)}
<Dropdown.Trigger
asChild
hideChevron
disabled={tablePath === removableTable}
>
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content menu PaperProps={{ className: 'w-52' }}>
{isGitHubConnected ? (
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
<IconButton
variant="borderless"
color={isSelected ? 'primary' : 'secondary'}
className={twMerge(
!isSelected &&
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
)}
>
<UsersIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>View Permissions</span>
</Dropdown.Item>
) : (
[
!isSelectedSchemaLocked && (
<Dropdown.Item
key="edit-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
openDrawer({
title: 'Edit Table',
component: (
<EditTableForm
onSubmit={async () => {
await queryClient.refetchQueries([
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
]);
await refetch();
}}
schema={table.table_schema}
table={table}
/>
),
})
}
>
<PencilIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Table</span>
</Dropdown.Item>
),
!isSelectedSchemaLocked && (
<Divider
key="edit-table-separator"
component="li"
/>
),
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-52' }}
>
{isGitHubConnected ? (
<Dropdown.Item
key="edit-permissions"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
>
@@ -453,68 +399,135 @@ function DataBrowserSidebarContent({
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Permissions</span>
</Dropdown.Item>,
!isSelectedSchemaLocked && (
<Divider
key="edit-permissions-separator"
component="li"
/>
),
!isSelectedSchemaLocked && (
<span>View Permissions</span>
</Dropdown.Item>
) : (
[
!isSelectedSchemaLocked && (
<Dropdown.Item
key="edit-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
openDrawer({
title: 'Edit Table',
component: (
<EditTableForm
onSubmit={async () => {
await queryClient.refetchQueries([
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
]);
await refetch();
}}
schema={table.table_schema}
table={table}
/>
),
})
}
>
<PencilIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Table</span>
</Dropdown.Item>
),
!isSelectedSchemaLocked && (
<Divider
key="edit-table-separator"
component="li"
/>
),
<Dropdown.Item
key="delete-table"
key="edit-permissions"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() =>
handleDeleteTableClick(
handleEditPermissionClick(
table.table_schema,
table.table_name,
)
}
>
<TrashIcon
<UsersIcon
className="h-4 w-4"
sx={{ color: 'error.main' }}
sx={{ color: 'text.secondary' }}
/>
<span>Delete Table</span>
</Dropdown.Item>
),
]
)}
</Dropdown.Content>
</Dropdown.Root>
}
>
<ListItem.Button
dense
selected={isSelected}
disabled={tablePath === removableTable}
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
sx={{
paddingRight:
(isSelected || isSidebarMenuOpen) &&
'2.25rem !important',
}}
component={NavLink}
href={`/${workspaceSlug}/${appSlug}/database/browser/default/${table.table_schema}/${table.table_name}`}
onClick={() => {
if (onSidebarItemClick) {
onSidebarItemClick(`default.${tablePath}`);
}
}}
<span>Edit Permissions</span>
</Dropdown.Item>,
!isSelectedSchemaLocked && (
<Divider
key="edit-permissions-separator"
component="li"
/>
),
!isSelectedSchemaLocked && (
<Dropdown.Item
key="delete-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() =>
handleDeleteTableClick(
table.table_schema,
table.table_name,
)
}
>
<TrashIcon
className="h-4 w-4"
sx={{ color: 'error.main' }}
/>
<span>Delete Table</span>
</Dropdown.Item>
),
]
)}
</Dropdown.Content>
</Dropdown.Root>
}
>
<ListItem.Text>{table.table_name}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
);
})}
</List>
)}
</nav>
</div>
<ListItem.Button
dense
selected={isSelected}
disabled={tablePath === removableTable}
className="group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
sx={{
paddingRight:
(isSelected || isSidebarMenuOpen) &&
'2.25rem !important',
}}
component={NavLink}
href={`/${workspaceSlug}/${appSlug}/database/browser/default/${table.table_schema}/${table.table_name}`}
onClick={() => {
if (onSidebarItemClick) {
onSidebarItemClick(`default.${tablePath}`);
}
}}
>
<ListItem.Text>{table.table_name}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
);
})}
</List>
)}
</nav>
</Box>
<Box className="border-t">
<ListItem.Button
dense
selected={asPath === sqlEditorHref}
className="flex border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
component={NavLink}
href={sqlEditorHref}
>
<div className="flex w-full flex-row items-center justify-center space-x-4">
<TerminalIcon />
<span className="flex">SQL Editor</span>
</div>
</ListItem.Button>
</Box>
</Box>
);
}
@@ -580,7 +593,7 @@ export default function DataBrowserSidebar({
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pt-2 pb-17 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:py-2.5 sm:transition-none',
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 pt-2 pb-17 motion-safe:transition-transform sm:relative sm:z-0 sm:h-full sm:pt-2.5 sm:pb-0 sm:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full sm:translate-x-0',
className,
)}

View File

@@ -0,0 +1,263 @@
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 { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlayIcon } from '@/components/ui/v2/icons/PlayIcon';
import { Input } from '@/components/ui/v2/Input';
import { Switch } from '@/components/ui/v2/Switch';
import { Table } from '@/components/ui/v2/Table';
import { TableBody } from '@/components/ui/v2/TableBody';
import { TableCell } from '@/components/ui/v2/TableCell';
import { TableHead } from '@/components/ui/v2/TableHead';
import { TableRow } from '@/components/ui/v2/TableRow';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useRunSQL } from '@/features/database/dataGrid/hooks/useRunSQL';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { PostgreSQL, sql } from '@codemirror/lang-sql';
import { useTheme } from '@mui/material';
import { githubDark, githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror from '@uiw/react-codemirror';
import { useCallback, useState } from 'react';
import { useResizable } from 'react-resizable-layout';
export default function SQLEditor() {
const theme = useTheme();
const isPlatform = useIsPlatform();
const [sqlCode, setSQLCode] = useState('');
const [track, setTrack] = useState(false);
const [cascade, setCascade] = useState(false);
const [readOnly, setReadOnly] = useState(false);
const [isMigration, setIsMigration] = useState(false);
const [migrationName, setMigrationName] = useState('');
const onChange = useCallback((value: string) => setSQLCode(value), []);
const { runSQL, loading, errorMessage, commandOk, rows, columns } = useRunSQL(
sqlCode,
track,
cascade,
readOnly,
isMigration,
migrationName,
);
const { position, separatorProps } = useResizable({
axis: 'y',
initial: 400,
min: 50,
reverse: true,
});
return (
<Box className="flex flex-1 flex-col justify-center overflow-hidden">
<Box className="flex flex-col space-y-2 border-b p-4">
<Text className="font-semibold">Raw SQL</Text>
<Box className="flex flex-col justify-between space-y-2 lg:flex-row lg:space-y-0 lg:space-x-4">
<Box className="flex w-full flex-col space-y-2 lg:flex-row lg:space-x-4 lg:space-y-0 xl:h-10">
<Box className="flex items-center space-x-2">
<Switch
label={
<Text variant="subtitle1" component="span">
Track this
</Text>
}
checked={track}
onChange={(event) => setTrack(event.currentTarget.checked)}
/>
<Tooltip title="If you are creating tables, views or functions, checking this will also expose them over the GraphQL API as top level fields. Functions only intended to be used as computed fields should not be tracked.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<Box className="flex items-center space-x-2">
<Switch
label={
<Text variant="subtitle1" component="span">
Cascade metadata
</Text>
}
checked={cascade}
onChange={(e) => setCascade(e.target.checked)}
/>
<Tooltip title="Cascade actions on all dependent metadata references, like relationships and permissions">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<Box className="flex items-center space-x-2">
<Switch
label={
<Text variant="subtitle1" component="span">
Read only
</Text>
}
checked={readOnly}
onChange={(e) => setReadOnly(e.target.checked)}
/>
<Tooltip title="When set to true, the request will be run in READ ONLY transaction access mode which means only select queries will be successful. This flag ensures that the GraphQL schema is not modified and is hence highly performant.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
{!isPlatform && (
<Box className="flex flex-col space-x-0 space-y-2 xl:flex-row xl:space-x-4 xl:space-y-0">
<Box className="flex items-center space-x-2">
<Switch
label={
<Text variant="subtitle1" component="span">
This is a migration
</Text>
}
checked={isMigration}
onChange={(e) => setIsMigration(e.target.checked)}
/>
<Tooltip title="Create a migration file with the SQL statement">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
{isMigration && (
<Input
name="isMigration"
id="isMigration"
placeholder="migration_name"
className="h-auto w-auto max-w-md"
fullWidth
hideEmptyHelperText
onChange={(e) => setMigrationName(e.target.value)}
/>
)}
</Box>
)}
</Box>
<Button
disabled={loading || !sqlCode.trim()}
variant="contained"
className="self-start"
startIcon={<PlayIcon />}
onClick={runSQL}
>
Run
</Button>
</Box>
</Box>
<CodeMirror
value={sqlCode}
height="100%"
className="min-h-[100px] flex-1 overflow-y-auto"
theme={theme.palette.mode === 'light' ? githubLight : githubDark}
extensions={[sql({ dialect: PostgreSQL })]}
onChange={onChange}
/>
<Box
className="h-2 border-t hover:cursor-row-resize"
sx={{
background: theme.palette.background.default,
}}
{...separatorProps}
/>
<Box
className="flex items-start overflow-auto p-4"
style={{ height: position }}
>
{loading && (
<ActivityIndicator
className="mx-auto self-center"
circularProgressProps={{
className: 'w-5 h-5',
}}
/>
)}
{errorMessage && (
<Alert
severity="error"
className="mx-auto grid grid-flow-row place-content-center gap-2 self-center"
>
<code>{errorMessage}</code>
</Alert>
)}
{!loading && !errorMessage && commandOk && (
<Alert
severity="success"
className="mx-auto grid grid-flow-row place-content-center gap-2 self-center"
>
<code>Success, no rows returned</code>
</Alert>
)}
{!loading && !errorMessage && (
<Table
style={{
tableLayout: 'auto',
}}
className="w-auto"
>
<TableHead
sx={{
background: theme.palette.background.default,
}}
>
<TableRow>
{columns.map((header) => (
<TableCell
key={header}
scope="col"
className="whitespace-nowrap border px-6 py-3 font-bold"
>
{header}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, rowIndex) => (
<TableRow
// eslint-disable-next-line react/no-array-index-key
key={String(rowIndex)}
// className="px-6 py-4 border whitespace-nowrap"
>
{row.map((value, valueIndex) => (
<TableCell
// eslint-disable-next-line react/no-array-index-key
key={`${value}-${valueIndex}`}
className="whitespace-nowrap border px-6 py-4"
>
{value}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)}
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,283 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { parseIdentifiersFromSQL } from '@/utils/sql';
import toast from 'react-hot-toast';
import { useState } from 'react';
export default function useRunSQL(
sqlCode: string,
track: boolean,
cascade: boolean,
readOnly: boolean,
isMigration: boolean,
migrationName: string,
) {
const { currentProject } = useCurrentWorkspaceAndProject();
const [loading, setLoading] = useState(false);
const [commandOk, setCommandOk] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [columns, setColumns] = useState<string[]>([]);
const [rows, setRows] = useState<string[][]>([[]]);
const appUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'hasura',
);
const adminSecret =
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret;
const toastStyle = getToastStyleProps();
const createMigration = async (
inputSQL: string,
migration: string,
isCascade: boolean,
) => {
try {
const migrationApiResponse = await fetch(`${appUrl}/apis/migrate`, {
method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify({
name: migration,
datasource: 'default',
up: [
{
type: 'run_sql',
args: {
source: 'default',
sql: inputSQL,
cascade: isCascade,
read_only: false,
},
},
],
down: [
{
type: 'run_sql',
args: {
source: 'default',
sql: '-- Could not auto-generate a down migration.',
cascade: isCascade,
read_only: false,
},
},
],
}),
});
if (!migrationApiResponse.ok) {
throw new Error('Migration API call failed');
}
return {
error: null,
};
} catch (createMigrationError) {
toast.error('An error happened when calling the migration API', {
style: toastStyle.style,
...toastStyle.error,
});
return {
error: createMigrationError,
};
}
};
const sendSQLToHasura = async (
inputSQL: string,
isCascade: boolean,
isReadOnly: boolean,
) => {
try {
if (!inputSQL) {
return {
result_type: 'error',
columns: [],
rows: [],
queryApiError: 'No SQL provided',
};
}
const response = await fetch(`${appUrl}/v2/query`, {
method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify({
type: 'run_sql',
args: {
source: 'default',
sql: inputSQL,
cascade: isCascade,
read_only: isReadOnly,
},
}),
});
if (!response.ok) {
const errorResponse = await response.json();
const queryApiError =
errorResponse?.internal?.error?.message || 'Unknown error';
return {
result_type: 'error',
columns: [],
rows: [],
error: queryApiError,
};
}
const responseBody = await response.json();
if (responseBody?.result_type === 'TuplesOk') {
return {
result_type: 'TuplesOk',
columns: responseBody.result[0],
rows: responseBody.result.slice(1),
error: '',
};
}
if (responseBody?.result_type === 'CommandOk') {
return {
result_type: 'CommandOk',
columns: [],
rows: [],
error: '',
};
}
// If the result_type is neither TuplesOk nor CommandOk
return {
result_type: 'error',
columns: [],
rows: [],
error: 'Unknown response type',
};
} catch (error) {
return {
result_type: 'error',
columns: [],
rows: [],
error: error.message || 'Unknown error',
};
}
};
const updateMetadata = async (inputSQL: string) => {
const entities = parseIdentifiersFromSQL(inputSQL);
const tablesOrViewEntities = entities.filter(
(entity) => entity.type !== 'function',
);
const functionEntities = entities.filter(
(entity) => entity.type === 'function',
);
const trackTablesOrViews = tablesOrViewEntities.map(({ name, schema }) => ({
type: 'pg_track_table',
args: {
source: 'default',
table: {
name,
schema,
},
},
}));
const trackFunctions = functionEntities.map(({ name, schema }) => ({
type: 'pg_track_function',
args: {
source: 'default',
function: {
name,
schema,
configuration: {},
},
},
}));
const metaDataPayload = {
source: 'default',
type: 'bulk',
args: [...trackTablesOrViews, ...trackFunctions],
};
try {
if (entities.length > 0) {
const metadataApiResponse = await fetch(`${appUrl}/v1/metadata`, {
method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify(metaDataPayload),
});
if (!metadataApiResponse.ok) {
throw new Error('Metadata API call failed');
}
}
} catch (error) {
toast.error('An error happened when calling the metadata API', {
style: toastStyle.style,
...toastStyle.error,
});
}
};
const runSQL = async () => {
setLoading(true);
setCommandOk(false);
setErrorMessage('');
if (isMigration) {
const { error: createMigrationError } = await createMigration(
sqlCode,
migrationName,
cascade,
);
setCommandOk(!createMigrationError);
if (createMigrationError) {
setErrorMessage('An unknown error occurred');
}
// if running the migration fails then we don't update the metadata
if (track && !createMigrationError) {
await updateMetadata(sqlCode);
}
} else {
const {
result_type,
error: $error,
columns: $columns,
rows: $rows,
} = await sendSQLToHasura(sqlCode, cascade, readOnly);
setCommandOk(result_type === 'CommandOk');
setColumns($columns);
setRows($rows);
setErrorMessage($error);
// if running the sql fails then we don't update the metadata
if (track && !$error) {
await updateMetadata(sqlCode);
}
}
setLoading(false);
};
return {
runSQL,
loading,
errorMessage,
commandOk,
rows,
columns,
};
}

View File

@@ -2,9 +2,9 @@ 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 { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
@@ -144,15 +144,11 @@ export default function AuthDomain() {
/>
</Box>
{!currentProject.plan.isFree && (
<Box
className="grid items-center grid-flow-col gap-1 p-3 rounded-lg shadow-sm place-content-between"
sx={{ backgroundColor: 'grey.200' }}
>
<Text>
Please note that once you increase the storage, it cannot be
reduced.
</Text>
</Box>
<Alert severity="info" className="col-span-6 text-left">
Note that volumes can only be increased (not decreased). Also, due
to an AWS limitation, the same volume can only be increased once
every 6 hours.
</Alert>
)}
</SettingsContainer>
</Form>

View File

@@ -0,0 +1,27 @@
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
// import { useTablePath } from '@/features/database/common/hooks/useTablePath';
import { DataBrowserLayout } from '@/features/database/dataGrid/components/DataBrowserLayout';
import { SQLEditor } from '@/features/database/dataGrid/components/SQLEditor';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type { ReactElement } from 'react';
export default function Editor() {
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
if (isPlatform && !currentProject?.config?.hasura.adminSecret) {
return <LoadingScreen />;
}
return (
<RetryableErrorBoundary>
<SQLEditor />
</RetryableErrorBoundary>
);
}
Editor.getLayout = function getLayout(page: ReactElement) {
return <DataBrowserLayout>{page}</DataBrowserLayout>;
};

View File

@@ -1,6 +1,7 @@
import { Container } from '@/components/layout/Container';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { AccountSettingsLayout } from '@/features/account/settings/components/AccountSettingsLayout';
import { DeleteAccount } from '@/features/account/settings/components/DeleteAccount';
import { PasswordSettings } from '@/features/account/settings/components/PasswordSettings';
import { PATSettings } from '@/features/account/settings/components/PATSettings';
import type { ReactElement } from 'react';
@@ -18,6 +19,8 @@ export default function AccountSettingsPage() {
<RetryableErrorBoundary>
<PATSettings />
</RetryableErrorBoundary>
<DeleteAccount />
</Container>
);
}

View File

@@ -22196,6 +22196,13 @@ export type Workspaces_Updates = {
where: Workspaces_Bool_Exp;
};
export type DeleteUserAccountMutationVariables = Exact<{
id: Scalars['uuid'];
}>;
export type DeleteUserAccountMutation = { __typename?: 'mutation_root', deleteUser?: { __typename: 'users' } | null };
export type GetPersonalAccessTokensQueryVariables = Exact<{ [key: string]: never; }>;
@@ -23190,6 +23197,39 @@ export const GetWorkspaceMembersWorkspaceMemberInviteFragmentDoc = gql`
memberType
}
`;
export const DeleteUserAccountDocument = gql`
mutation deleteUserAccount($id: uuid!) {
deleteUser(id: $id) {
__typename
}
}
`;
export type DeleteUserAccountMutationFn = Apollo.MutationFunction<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
/**
* __useDeleteUserAccountMutation__
*
* To run a mutation, you first call `useDeleteUserAccountMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteUserAccountMutation` 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 [deleteUserAccountMutation, { data, loading, error }] = useDeleteUserAccountMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>(DeleteUserAccountDocument, options);
}
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const GetPersonalAccessTokensDocument = gql`
query GetPersonalAccessTokens {
personalAccessTokens: authRefreshTokens(

View File

@@ -0,0 +1,53 @@
// The parsing was inspired by code from the hasura/graphql-engine repo
export interface ParsedSQLEntity {
type: string;
name: string;
schema: string;
}
const sanitizeValue = (value: string) => {
let val = value;
if (!/^".*"$/.test(value)) {
val = value?.toLowerCase() ?? '';
}
return val.replace(/['"]+/g, '');
};
const stripComments = (sql: string) => {
const regExp = /(--[^\r\n]*)|(\/\*[\w\W]*?(?=\*\/)\*\/)/; // eslint-disable-line
const comments = sql.match(new RegExp(regExp, 'gmi'));
if (!comments?.length) {
return sql;
}
return comments.reduce(
(acc: string, comment: string) => acc.replace(comment, ''),
sql,
);
};
export const parseIdentifiersFromSQL = (sql: string): ParsedSQLEntity[] => {
const objects: ParsedSQLEntity[] = [];
const sanitizedSql = stripComments(sql);
const regExp =
/create\s*(?:|or\s*replace)\s*(?<type>view|table|function)\s*(?:\s*if*\s*not\s*exists\s*)?((?<schema>\"?\w+\"?)\.(?<nameWithSchema>\"?\w+\"?)|(?<name>\"?\w+\"?))\s*(?<partition>partition\s*of)?/gim; // eslint-disable-line
Array.from(sanitizedSql.matchAll(regExp)).forEach((result) => {
const { type, schema, name, nameWithSchema } = result.groups ?? {};
if (type && (name || nameWithSchema)) {
objects.push({
type: type.toLowerCase(),
schema: sanitizeValue(schema || 'public'),
name: sanitizeValue(name || nameWithSchema),
});
}
});
return objects;
};

View File

@@ -1,5 +1,17 @@
# @nhost/docs
## 0.7.2
### Patch Changes
- 138bf9eb5: fix: add instructions for enabling Sign In with LinkedIn using OpenID Connect
## 0.7.1
### Patch Changes
- 1ee021b4a: remove custom domains from roadmap
## 0.7.0
### Minor Changes

View File

@@ -36,6 +36,14 @@ Follow this guide to sign in users with LinkedIn.
- Copy and paste the **OAuth Callback URL** from Nhost.
- Click **Update**.
## Enable Sign In with LinkedIn using OpenID Connect
- Click on **Products** in the top menu.
- Scroll down and look for **Sign In with LinkedIn using OpenID Connect**.
- Click **Request Access**.
- Click the checkbox **I have read and agree to these terms**.
- Click **Request Access**.
## Configure Nhost
- Copy and paste the **Client ID** and **Client Secret** from LinkedIn to your Nhost OAuth settings for LinkedIn.

View File

@@ -43,7 +43,6 @@ Nhost Run works with container images built for the **arm architecture**. Images
Some missing functionality we are currently working on and should be added soon:
1. Custom domains
2. Run services with the CLI alongside your project
3. Ability to connect services to repositories for automated building and deployment (currently this needs to be done via a third party CI, see [Deployment via CI](/run/ci) for more details).
4. Expose TCP/UDP ports
1. Run services with the CLI alongside your project
2. Ability to connect services to repositories for automated building and deployment (currently this needs to be done via a third party CI, see [Deployment via CI](/run/ci) for more details).
3. Expose TCP/UDP ports

View File

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

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.1'

View File

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

View File

@@ -1,5 +1,12 @@
# @nhost-examples/nextjs-server-components
## 0.1.1
### Patch Changes
- Updated dependencies [8b127fbb6]
- @nhost/nhost-js@2.2.18
## 0.1.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -5,7 +5,7 @@ import { getNhost } from '@utils/nhost'
import Head from 'next/head'
import Link from 'next/link'
const PAT = async ({
const PATs = async ({
params
}: {
params: {
@@ -100,4 +100,4 @@ const PAT = async ({
)
}
export default withAuthAsync(PAT)
export default withAuthAsync(PATs)

View File

@@ -54,6 +54,7 @@ const TodoItem = ({ todo }: { todo: Todo }) => {
<Link
className="w-6 h-6"
target="_blank"
passHref
href={nhost.storage.getPublicUrl({ fileId: todo.attachment.id })}
>
<svg

View File

@@ -1,4 +1,6 @@
import { manageAuthSession } from '@utils/nhost'
// eslint-disable-next-line @next/next/no-server-import-in-page
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {

View File

@@ -1,5 +1,7 @@
import { AuthErrorPayload, NhostClient, NhostSession } from '@nhost/nhost-js'
import { cookies } from 'next/headers'
// eslint-disable-next-line @next/next/no-server-import-in-page
import { NextRequest, NextResponse } from 'next/server'
import { type StateFrom } from 'xstate/lib/types'
import { waitFor } from 'xstate/lib/waitFor'

View File

@@ -0,0 +1,42 @@
table:
name: virus
schema: storage
configuration:
column_config:
created_at:
custom_name: createdAt
file_id:
custom_name: fileId
filename:
custom_name: filename
id:
custom_name: id
updated_at:
custom_name: updatedAt
user_session:
custom_name: userSession
virus:
custom_name: virus
custom_column_names:
created_at: createdAt
file_id: fileId
filename: filename
id: id
updated_at: updatedAt
user_session: userSession
virus: virus
custom_name: virus
custom_root_fields:
delete: deleteViruses
delete_by_pk: deleteVirus
insert: insertViruses
insert_one: insertVirus
select: viruses
select_aggregate: virusesAggregate
select_by_pk: virus
update: updateViruses
update_by_pk: updateVirus
object_relationships:
- name: file
using:
foreign_key_constraint_on: file_id

View File

@@ -10,3 +10,4 @@
- "!include public_todos.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"
- "!include storage_virus.yaml"

View File

@@ -28,10 +28,11 @@ httpPoolSize = 100
version = 18
[auth]
version = '0.21.3'
version = '0.22.1'
[auth.redirections]
clientUrl = 'http://localhost:3000'
allowedUrls = ['https://example-nextjs-server-components.nhost.io', 'https://example-sveltekit.nhost.io']
[auth.signUp]
enabled = true
@@ -144,7 +145,7 @@ version = '14.6-20230406-2'
[provider]
[storage]
version = '0.3.5'
version = '0.4.0'
[observability]
[observability.grafana]

View File

@@ -15,7 +15,7 @@
"postinstall": "pnpm add-nhost-js"
},
"devDependencies": {
"@nhost/nhost-js": "2.2.17",
"@nhost/nhost-js": "2.2.18",
"@playwright/test": "^1.31.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
@@ -36,7 +36,6 @@
},
"type": "module",
"dependencies": {
"@apollo/client": "^3.8.1",
"graphql": "^16.7.1",
"graphql-tag": "^2.12.6",
"js-cookie": "^3.0.5",

View File

@@ -25,7 +25,7 @@ export default defineConfig({
/* 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',
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
@@ -51,7 +51,7 @@ export default defineConfig({
webServer: {
command: 'pnpm dev',
url: 'http://localhost:5173',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
}
})

View File

@@ -11,8 +11,8 @@ dependencies:
devDependencies:
'@nhost/nhost-js':
specifier: 2.2.17
version: 2.2.17(graphql@16.8.0)
specifier: 2.2.18
version: 2.2.18(graphql@16.8.0)
packages:
@@ -58,8 +58,8 @@ packages:
- encoding
dev: true
/@nhost/nhost-js@2.2.17(graphql@16.8.0):
resolution: {integrity: sha512-6KRzhqmx7JcOmbp91/YZaBavGKdyGdx7kDrzRLoP1RYYOAIMpdMhHzeIju9LQfujY/8nFARRq97vFpPSbpnhSg==}
/@nhost/nhost-js@2.2.18(graphql@16.8.0):
resolution: {integrity: sha512-aHn6p75fuG7SEUyB/yfX5TXtVTqwCT88zdN9Mmgo/8hnFOGV1XM7B4fxuGpNQCz18tG6kjM24tWx8EGXAEZ1sw==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
dependencies:

View File

@@ -3,10 +3,14 @@ import { redirect } from '@sveltejs/kit'
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ cookies }) => {
default: async ({ request, cookies }) => {
const nhost = await getNhost(cookies)
const { providerUrl } = await nhost.auth.signIn({ provider: 'google' })
const { providerUrl } = await nhost.auth.signIn({
provider: 'google',
options: {
redirectTo: new URL(request.url).origin
}
})
if (providerUrl) {
throw redirect(307, providerUrl)

View File

@@ -1,5 +1,5 @@
import { getNhost } from '$lib/nhost'
import { gql } from '@apollo/client'
import gql from 'graphql-tag'
/** @type {import('./$types').PageServerLoad} */
export const load = async ({ url, cookies }) => {

View File

@@ -1,6 +1,6 @@
import { getNhost } from '$lib/nhost.js'
import { gql } from '@apollo/client'
import { json, redirect } from '@sveltejs/kit'
import gql from 'graphql-tag'
export const DELETE = async ({ cookies, params }) => {
const nhost = await getNhost(cookies)

View File

@@ -1,5 +1,5 @@
import { getNhost } from '$lib/nhost'
import { gql } from '@apollo/client'
import gql from 'graphql-tag'
/** @type {import('./$types').PageServerLoad} */
export const load = async ({ url, cookies }) => {

View File

@@ -1,6 +1,6 @@
import { getNhost } from '$lib/nhost'
import { gql } from '@apollo/client'
import { redirect } from '@sveltejs/kit'
import gql from 'graphql-tag'
/** @type {import('./$types').Actions} */
export const actions = {

View File

@@ -1,6 +1,6 @@
import { getNhost } from '$lib/nhost'
import { gql } from '@apollo/client'
import { redirect } from '@sveltejs/kit'
import gql from 'graphql-tag'
/** @type {import('./$types').Actions} */
export const actions = {

View File

@@ -1,6 +1,6 @@
import { getNhost } from '$lib/nhost'
import { gql } from '@apollo/client'
import { json } from '@sveltejs/kit'
import gql from 'graphql-tag'
/** @type {import('./$types').RequestHandler} */
export async function POST({ request, cookies }) {

View File

@@ -1,5 +1,19 @@
# @nhost-examples/react-apollo
## 0.1.17
### Patch Changes
- 67b2c044b: feat: add sign-in with Linked-In
## 0.1.16
### Patch Changes
- 6e61dce29: feat: add SignIn with Apple
- @nhost/react@2.1.1
- @nhost/react-apollo@6.0.1
## 0.1.15
### Patch Changes

View File

@@ -25,14 +25,14 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.21.2'
version = '0.22.1'
[auth.redirections]
clientUrl = 'https://react-apollo.example.nhost.io/'
allowedUrls = ['https://react-apollo.example.nhost.io/profile']
allowedUrls = ['https://react-apollo.example.nhost.io/profile', 'http://localhost:30000']
[auth.signUp]
enabled = true
@@ -79,7 +79,11 @@ enabled = false
[auth.method.oauth]
[auth.method.oauth.apple]
enabled = false
enabled = true
clientId = '{{ secrets.APPLE_SERVICE_IDENTIFIER }}'
keyId = '{{ secrets.APPLE_KEY_ID }}'
teamId = '{{ secrets.APPLE_TEAM_ID }}'
privateKey = '{{ secrets.APPLE_PRIVATE_KEY }}'
[auth.method.oauth.azuread]
tenant = 'common'
@@ -108,7 +112,9 @@ clientId = '{{ secrets.GOOGLE_CLIENT_ID }}'
clientSecret = '{{ secrets.GOOGLE_CLIENT_SECRET }}'
[auth.method.oauth.linkedin]
enabled = false
enabled = true
clientId='{{ secrets.LINKEDIN_CLIENT_ID }}'
clientSecret='{{ secrets.LINKEDIN_CLIENT_SECRET }}'
[auth.method.oauth.spotify]
enabled = false

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-apollo",
"version": "0.1.15",
"version": "0.1.17",
"private": true,
"dependencies": {
"@apollo/client": "^3.7.14",

View File

@@ -1,12 +1,16 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@apollo/client':
specifier: ^3.7.14
version: 3.7.14(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0)(subscriptions-transport-ws@0.9.19)
'@mantine/core':
specifier: ^4.2.12
version: 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
version: 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@mantine/dropzone':
specifier: ^4.2.12
version: 4.2.12(@mantine/core@4.2.12)(@mantine/hooks@4.2.12)(react-dom@18.2.0)(react@18.2.0)
@@ -21,7 +25,7 @@ dependencies:
version: 4.2.12(@mantine/core@4.2.12)(@mantine/hooks@4.2.12)(react-dom@18.2.0)(react@18.2.0)
'@nhost/react':
specifier: '*'
version: 0.2.0(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(xstate@4.37.2)
version: 0.2.0(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)(xstate@4.37.2)
'@nhost/react-apollo':
specifier: '*'
version: 1.0.1(@apollo/client@3.7.14)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0)
@@ -64,11 +68,11 @@ devDependencies:
specifier: ^6.0.1
version: 6.0.1
'@types/react':
specifier: ^18.2.6
version: 18.2.6
specifier: ^18.2.14
version: 18.2.34
'@types/react-dom':
specifier: ^18.2.4
version: 18.2.4
specifier: ^18.2.6
version: 18.2.14
'@types/totp-generator':
specifier: ^0.0.4
version: 0.0.4
@@ -419,7 +423,7 @@ packages:
resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==}
dev: false
/@emotion/react@11.7.1(@babel/core@7.22.1)(@types/react@18.2.6)(react@18.2.0):
/@emotion/react@11.7.1(@babel/core@7.22.1)(@types/react@18.2.34)(react@18.2.0):
resolution: {integrity: sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==}
peerDependencies:
'@babel/core': ^7.0.0
@@ -438,7 +442,7 @@ packages:
'@emotion/sheet': 1.2.2
'@emotion/utils': 1.0.0
'@emotion/weak-memoize': 0.2.5
'@types/react': 18.2.6
'@types/react': 18.2.34
hoist-non-react-statics: 3.3.2
react: 18.2.0
dev: false
@@ -450,7 +454,7 @@ packages:
'@emotion/memoize': 0.7.5
'@emotion/unitless': 0.7.5
'@emotion/utils': 1.0.0
csstype: 3.0.9
csstype: 3.1.2
dev: false
/@emotion/sheet@1.2.2:
@@ -1128,7 +1132,7 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@mantine/core@4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
/@mantine/core@4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-PZcVUvcSZiZmLR1moKBJFdFIh6a4C+TE2ao91kzTAlH5Qb8t/V3ONbfPk3swHoYr7OSLJQM8vZ7UD5sFDiq0/g==}
peerDependencies:
'@mantine/hooks': 4.2.12
@@ -1136,13 +1140,13 @@ packages:
react-dom: '>=16.8.0'
dependencies:
'@mantine/hooks': 4.2.12(react@18.2.0)
'@mantine/styles': 4.2.12(@babel/core@7.22.1)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mantine/styles': 4.2.12(@babel/core@7.22.1)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@popperjs/core': 2.11.8
'@radix-ui/react-scroll-area': 0.1.4(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
react-textarea-autosize: 8.4.1(@types/react@18.2.6)(react@18.2.0)
react-textarea-autosize: 8.4.1(@types/react@18.2.34)(react@18.2.0)
transitivePeerDependencies:
- '@babel/core'
- '@types/react'
@@ -1156,7 +1160,7 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 4.2.12(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1179,7 +1183,7 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 4.2.12(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1194,21 +1198,21 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mantine/core': 4.2.12(@babel/core@7.22.1)(@mantine/hooks@4.2.12)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 4.2.12(react@18.2.0)
prism-react-renderer: 1.3.5(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@mantine/styles@4.2.12(@babel/core@7.22.1)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
/@mantine/styles@4.2.12(@babel/core@7.22.1)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-9q1DzW0UNW/ORMGLHfN2XABOSEm0ZQebhNlLD757R6OQouoLuUf9elUwgGOXSyogMlsAYoy84XbJ3ZbbTm4YCA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@emotion/cache': 11.7.1
'@emotion/react': 11.7.1(@babel/core@7.22.1)(@types/react@18.2.6)(react@18.2.0)
'@emotion/react': 11.7.1(@babel/core@7.22.1)(@types/react@18.2.34)(react@18.2.0)
'@emotion/serialize': 1.0.2
'@emotion/utils': 1.0.0
clsx: 1.2.1
@@ -1256,14 +1260,14 @@ packages:
- utf-8-validate
dev: false
/@nhost/react@0.2.0(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(xstate@4.37.2):
/@nhost/react@0.2.0(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)(xstate@4.37.2):
resolution: {integrity: sha512-V8um4+YVN2dNio8u+zKlxHIub7ZU4Cz2D3wf2dJW+fHHgChh5niNw4vQ3L+JldGe4yAXATF94P8VaQav/Ksgqg==}
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
dependencies:
'@nhost/client': 0.2.0
'@xstate/react': 2.0.1(@types/react@18.2.6)(react@18.2.0)(xstate@4.37.2)
'@xstate/react': 2.0.1(@types/react@18.2.34)(react@18.2.0)(xstate@4.37.2)
immer: 9.0.21
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -1501,14 +1505,14 @@ packages:
/@types/prop-types@15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
/@types/react-dom@18.2.4:
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
/@types/react-dom@18.2.14:
resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==}
dependencies:
'@types/react': 18.2.6
'@types/react': 18.2.34
dev: true
/@types/react@18.2.6:
resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==}
/@types/react@18.2.34:
resolution: {integrity: sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.3
@@ -1643,7 +1647,7 @@ packages:
xstate: 4.37.2
dev: true
/@xstate/react@2.0.1(@types/react@18.2.6)(react@18.2.0)(xstate@4.37.2):
/@xstate/react@2.0.1(@types/react@18.2.34)(react@18.2.0)(xstate@4.37.2):
resolution: {integrity: sha512-sT3hxyzNBw+bm7uT3BP+uXzN0MnRqiaj/U9Yl4OYaMAUJXWsRvSA/ipL7EDf0gVLRGrRhJTCsC0cjWaduAAqnw==}
peerDependencies:
'@xstate/fsm': ^1.6.5
@@ -1656,7 +1660,7 @@ packages:
optional: true
dependencies:
react: 18.2.0
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.6)(react@18.2.0)
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.34)(react@18.2.0)
use-subscription: 1.8.0(react@18.2.0)
xstate: 4.37.2
transitivePeerDependencies:
@@ -3174,7 +3178,7 @@ packages:
react: 18.2.0
dev: false
/react-textarea-autosize@8.4.1(@types/react@18.2.6)(react@18.2.0):
/react-textarea-autosize@8.4.1(@types/react@18.2.34)(react@18.2.0):
resolution: {integrity: sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==}
engines: {node: '>=10'}
peerDependencies:
@@ -3183,7 +3187,7 @@ packages:
'@babel/runtime': 7.22.3
react: 18.2.0
use-composed-ref: 1.3.0(react@18.2.0)
use-latest: 1.2.1(@types/react@18.2.6)(react@18.2.0)
use-latest: 1.2.1(@types/react@18.2.34)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -3637,7 +3641,7 @@ packages:
react: 18.2.0
dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.2.6)(react@18.2.0):
/use-isomorphic-layout-effect@1.1.2(@types/react@18.2.34)(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
@@ -3646,11 +3650,11 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.6
'@types/react': 18.2.34
react: 18.2.0
dev: false
/use-latest@1.2.1(@types/react@18.2.6)(react@18.2.0):
/use-latest@1.2.1(@types/react@18.2.34)(react@18.2.0):
resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==}
peerDependencies:
'@types/react': '*'
@@ -3659,9 +3663,9 @@ packages:
'@types/react':
optional: true
dependencies:
'@types/react': 18.2.6
'@types/react': 18.2.34
react: 18.2.0
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.6)(react@18.2.0)
use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.34)(react@18.2.0)
dev: false
/use-subscription@1.8.0(react@18.2.0):

View File

@@ -1,11 +1,14 @@
import { FaGithub, FaGoogle } from 'react-icons/fa/index.js'
import { FaApple, FaGithub, FaGoogle, FaLinkedin } from 'react-icons/fa/index.js'
import { useProviderLink } from '@nhost/react'
import AuthLink from './AuthLink'
export default function OauthLinks() {
const { github, google } = useProviderLink({ redirectTo: window.location.origin })
const { github, google, apple, linkedin } = useProviderLink({
redirectTo: window.location.origin
})
return (
<>
<AuthLink leftIcon={<FaGithub />} link={github} color="#333">
@@ -14,6 +17,13 @@ export default function OauthLinks() {
<AuthLink leftIcon={<FaGoogle />} link={google} color="#de5246">
Continue with Google
</AuthLink>
<AuthLink leftIcon={<FaApple />} link={apple} color="#333333">
Sign In With Apple
</AuthLink>
<AuthLink leftIcon={<FaLinkedin />} link={linkedin} color="#0073B1">
Sign In With LinkedIn
</AuthLink>
</>
)
}

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -28,7 +28,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -1,5 +1,13 @@
# @nhost-examples/vue-apollo
## 0.0.9
### Patch Changes
- 0c49e757c: feat: add new Storage page to demonstrate how to use the composables `useMultipleFilesUpload` together with `useFileUploadItem`
- Updated dependencies [0c49e757c]
- @nhost/vue@1.14.0
## 0.0.8
### Patch Changes

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/vue-apollo",
"private": true,
"version": "0.0.8",
"version": "0.0.9",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -17,14 +17,16 @@
"@apollo/client": "^3.7.1",
"@mdi/font": "5.9.55",
"@nhost/apollo": "*",
"@nhost/nhost-js": "*",
"@nhost/vue": "*",
"@vue/apollo-composable": "4.0.0-alpha.18",
"graphql": "15.7.2",
"graphql": "^16.8.1",
"graphql-tag": "^2.12.6",
"roboto-fontface": "*",
"vite-plugin-vuetify": "^1.0.1",
"vue": "^3.2.41",
"vue-router": "^4.1.6",
"vue3-dropzone": "^2.1.2",
"vuetify": "3.0.0-beta.10",
"webfontloader": "^1.0.0"
},
@@ -35,7 +37,7 @@
"@xstate/inspect": "^0.6.2",
"sass": "1.32.0",
"typescript": "4.9.4",
"vite": "^4.0.2",
"vite": "^4.5.0",
"vue-tsc": "^0.38.9"
},
"eslintConfig": {

1657
examples/vue-apollo/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import { ref, toRaw, unref, type Ref } from 'vue'
import { useDropzone, type FileRejectReason } from 'vue3-dropzone'
const { multiple } = defineProps({
multiple: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['onDrop'])
const files: Ref<File[]> = ref([])
const errors: Ref<FileRejectReason[]> = ref([])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple
})
function onDrop(acceptFiles: File[], rejectReasons: FileRejectReason[]) {
files.value = acceptFiles
errors.value = rejectReasons
emit('onDrop', toRaw(unref(files)))
}
</script>
<template>
<div>
<div class="dropzone" v-bind="getRootProps()">
<div
class="border"
:class="{
isDragActive
}"
>
<input v-bind="getInputProps()" />
<p v-if="isDragActive">Drop here ...</p>
<p v-else>Drag and drop here, or Click to select</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dropzone,
.files {
width: 100%;
margin: 0 auto;
padding: 10px;
border-radius: 8px;
box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
font-size: 12px;
line-height: 1.5;
}
.border {
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
transition: all 0.3s ease;
background: #fff;
&.isDragActive {
border: 2px dashed #ffb300;
background: rgb(255 167 18 / 20%);
}
}
.file-item {
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
background: rgb(255 167 18 / 20%);
padding: 7px;
padding-left: 15px;
margin-top: 10px;
&:first-child {
margin-top: 0;
}
.delete-file {
background: red;
color: #fff;
padding: 5px 10px;
border-radius: 8px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { FileItemRef } from '@nhost/nhost-js'
import { useFileUploadItem } from '@nhost/vue'
const { file } = defineProps<{ file: FileItemRef }>()
const { name, progress } = useFileUploadItem(file)
</script>
<template>
<div class="container">
<span class="file_name">{{ name }}</span>
<v-progress-linear v-model="progress" color="green">
{{ progress }}
</v-progress-linear>
</div>
</template>
<style lang="css" scoped>
.container {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 1rem;
}
.file_name {
margin-right: 1rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 40%;
}
</style>

View File

@@ -3,8 +3,14 @@
<v-list-item title="Home" to="/" value="home" prepend-icon="mdi-home" />
<v-list-item title="Profile" to="/profile" value="profile" prepend-icon="mdi-account" />
<v-list-item title="Apollo" to="/apollo" value="apollo" prepend-icon="mdi-api" />
<v-list-item title="Storage" to="/storage" value="storage" prepend-icon="mdi-server" />
<v-list-item title="About" to="/about" value="about" prepend-icon="mdi-information" />
<v-list-item v-if="authenticated" title="Sign out" prepend-icon="mdi-exit-to-app" @click="signOutHandler" />
<v-list-item
v-if="authenticated"
title="Sign out"
prepend-icon="mdi-exit-to-app"
@click="signOutHandler"
/>
</v-list>
</template>

View File

@@ -0,0 +1,129 @@
<template>
<div className="d-flex align-center flex-column">
<h1>Storage</h1>
<v-card width="400" class="singleFileUpload" tile>
<v-card-title>Upload a single file</v-card-title>
<v-card-text>
<FileDropZone @on-drop="onDropSingleFile" />
<span v-if="isUploading" class="upload_status">Uploading...</span>
<span v-if="isUploaded" class="upload_status upload_success">Upload Succeeded</span>
<span v-if="isError" class="upload_status upload_error">Upload Failed</span>
<div v-if="fileToUpload" class="file_progress">
<span class="file_progress_name">{{ fileToUpload.name }}</span>
<v-progress-linear v-model="progress" color="green">
{{ progress }}
</v-progress-linear>
<button class="clearButton" @click="clearFile">Clear</button>
</div>
</v-card-text>
</v-card>
<v-card width="400" tile class="relative">
<v-card-title>Upload multiple files</v-card-title>
<v-card-text class="footer">
<FileDropZone :multiple="true" @on-drop="onDropMultipleFiles" />
<span v-if="isUploadingAll" class="upload_status">Uploading...</span>
<span v-if="isUploadedAll" class="upload_status upload_success">Upload Succeeded</span>
<span v-if="isErrorAll" class="upload_status upload_error">Upload Failed</span>
</v-card-text>
<div v-for="(file, index) of files" :key="index">
<FileUploadItem :file="file" />
</div>
<div class="relative buttonsContainer">
<v-btn
class="mb-2 text-white w-100"
:disabled="!files.length"
variant="elevated"
color="green"
@click="uploadAll"
>
Upload
</v-btn>
<v-btn class="w-100" @click="clear"> Clear </v-btn>
</div>
</v-card>
</div>
</template>
<script lang="ts" setup>
import { useFileUpload, useMultipleFilesUpload } from '@nhost/vue'
import { ref } from 'vue'
import FileDropZone from '../components/FileDropZone.vue'
import FileUploadItem from '../components/FileUploadItem.vue'
const fileToUpload = ref<File | null>(null)
const { upload, progress, isUploaded, isUploading, isError } = useFileUpload()
const onDropSingleFile = async ([file]: File[]) => {
fileToUpload.value = file
upload({ file })
}
const clearFile = () => {
fileToUpload.value = null
isUploaded.value = false
}
const {
add,
upload: uploadAll,
isUploaded: isUploadedAll,
isUploading: isUploadingAll,
isError: isErrorAll,
files,
clear
} = useMultipleFilesUpload()
const onDropMultipleFiles = (files: File[]) => {
add({ files })
}
</script>
<style lang="css" scoped>
.buttonsContainer {
padding: 1rem;
}
.upload_success {
color: 'green';
}
.upload_error {
color: 'red';
}
.file_progress {
display: flex;
flex-direction: row;
align-items: center;
}
.file_progress_name {
margin-right: 1rem;
}
.upload_status {
display: block;
margin: 1rem 0;
}
.singleFileUpload {
margin-bottom: 1rem;
}
.clearButton {
margin-left: 1rem;
background: red;
color: #fff;
padding: 5px 10px;
border-radius: 8px;
cursor: pointer;
}
.relative {
position: relative;
}
</style>

View File

@@ -13,6 +13,7 @@ import SignUpEmailPasword from './pages/sign-up/EmailPassword.vue'
import SignUpEmailPaswordless from './pages/sign-up/EmailPasswordless.vue'
import SignUp from './pages/sign-up/IndexPage.vue'
import Signout from './pages/SignoutPage.vue'
import StoragePage from './pages/StoragePage.vue'
export const routes: RouteRecordRaw[] = [
{ path: '/', component: Index, meta: { auth: true } },
@@ -50,5 +51,6 @@ export const routes: RouteRecordRaw[] = [
}
]
},
{ path: '/apollo', component: ApolloPage, meta: { auth: true } }
{ path: '/apollo', component: ApolloPage, meta: { auth: true } },
{ path: '/storage', component: StoragePage, meta: { auth: true } }
]

View File

@@ -1,5 +1,12 @@
# @nhost/apollo
## 5.2.22
### Patch Changes
- Updated dependencies [8b127fbb6]
- @nhost/nhost-js@2.2.18
## 5.2.21
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/apollo",
"version": "5.2.21",
"version": "5.2.22",
"description": "Nhost Apollo Client library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,12 @@
# @nhost/react-apollo
## 6.0.1
### Patch Changes
- @nhost/apollo@5.2.22
- @nhost/react@2.1.1
## 6.0.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-apollo",
"version": "6.0.0",
"version": "6.0.1",
"description": "Nhost React Apollo client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/react-urql
## 3.0.1
### Patch Changes
- @nhost/react@2.1.1
## 3.0.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-urql",
"version": "3.0.0",
"version": "3.0.1",
"description": "Nhost React URQL client",
"license": "MIT",
"keywords": [

View File

@@ -0,0 +1,805 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__elements": {},
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "9.2.0"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 6,
"panels": [],
"title": "General",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Number of requests by method/function",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by(method, ingress) (increase(nginx_ingress_controller_requests{method=~\"$method\",ingress=~\"$ingress\"}[$__rate_interval]))",
"format": "time_series",
"interval": "2m",
"legendFormat": "{{method}} {{ingress}}",
"range": true,
"refId": "A"
}
],
"title": "Requests",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Number of requests by status response",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 1
},
"id": 21,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by(status) (increase(nginx_ingress_controller_requests{method=~\"$method\",ingress=~\"$ingress\"}[$__rate_interval]))",
"format": "time_series",
"interval": "2m",
"legendFormat": "{{status}}",
"range": true,
"refId": "A"
}
],
"title": "Response Status",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "bytes"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 9
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by(ingress, method) (increase(nginx_ingress_controller_response_size_sum{ingress=~\"$ingress\",method=~\"$method\"}[$__rate_interval])) / sum by(ingress, method) (increase(nginx_ingress_controller_requests{ingress=~\"$ingress\",method=~\"$method\"}[$__rate_interval]))",
"interval": "2m",
"legendFormat": "{{ method }} - {{ ingress }}",
"range": true,
"refId": "A"
}
],
"title": "Average Response Size",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"filterable": true,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "custom.width",
"value": 183
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 9
},
"id": 4,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Value"
}
]
},
"pluginVersion": "9.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": false,
"expr": "ceil(sum by(ingress, method) (increase(nginx_ingress_controller_requests{ingress=~\"$ingress\",method=~\"$method\"}[$__range])))",
"format": "table",
"instant": true,
"interval": "2m",
"legendFormat": "{{ingress}} {{method}}",
"range": false,
"refId": "A"
}
],
"title": "Total Requests",
"transformations": [],
"type": "table"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 17
},
"id": 26,
"panels": [],
"title": "Response Times",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 18
},
"id": 11,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by(method, ingress) (increase(nginx_ingress_controller_response_duration_seconds_sum{method=~\"$method\", ingress=~\"$ingress\"}[$__rate_interval])) / sum by(method, ingress) (increase(nginx_ingress_controller_response_duration_seconds_count{method=~\"$method\", ingress=~\"$ingress\"}[$__rate_interval]))",
"interval": "2m",
"legendFormat": "{{ ingress }} - {{ method }}",
"range": true,
"refId": "A"
}
],
"title": "Average Response Time",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 26
},
"id": 19,
"panels": [],
"title": "Errors",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"description": "Number of requests that failed divided by the total number of requests",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percentunit"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 27
},
"id": 20,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"expr": "sum by(ingress,method) (increase(nginx_ingress_controller_requests{ingress=~\"$ingress\",method=~\"$method\",status=~\"^[4-5].*\"}[$__rate_interval])) / sum by(ingress, method) (increase(nginx_ingress_controller_requests[$__rate_interval]))",
"format": "time_series",
"interval": "2m",
"legendFormat": "{{method}} {{ ingress }}",
"range": true,
"refId": "A"
}
],
"title": "Error Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"filterable": true,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Time"
},
"properties": [
{
"id": "custom.width",
"value": 183
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 27
},
"id": 10,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Value"
}
]
},
"pluginVersion": "9.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"editorMode": "code",
"exemplar": false,
"expr": "ceil(sum by(ingress, method,status) (increase(nginx_ingress_controller_requests{method=~\"$method\",ingress=~\"$ingress\",status=~\"^[4-5].*\"}[$__range])))",
"format": "table",
"instant": true,
"interval": "2m",
"legendFormat": "{{ingress}} {{method}}",
"range": false,
"refId": "A"
}
],
"title": "Total Errors",
"transformations": [],
"type": "table"
}
],
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {},
"definition": "label_values(nginx_ingress_controller_requests,ingress)",
"hide": 0,
"includeAll": true,
"multi": false,
"name": "ingress",
"options": [],
"query": {
"query": "label_values(nginx_ingress_controller_requests,ingress)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"current": {},
"definition": "label_values(nginx_ingress_controller_requests,method)",
"hide": 0,
"includeAll": true,
"multi": false,
"name": "method",
"options": [],
"query": {
"query": "label_values(nginx_ingress_controller_requests,method)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Ingress Metrics",
"uid": "WOWEHb7Sz",
"version": 16,
"weekStart": ""
}

View File

@@ -21,8 +21,8 @@
"build:docs": "turbo run build --filter=@nhost/docs",
"build:all": "turbo run build --include-dependencies",
"dev": "turbo run dev --filter=!@nhost/dashboard --filter=!@nhost/docs --filter=!@nhost-examples/* --filter=!@nhost/docgen --no-deps --include-dependencies",
"clean:all": "pnpm clean && rm -rf ./{{packages,examples}/*,docs,dashboard}/{.nhost,node_modules} node_modules",
"clean": "rm -rf ./{{packages,examples}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
"clean:all": "pnpm clean && rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{.nhost,node_modules} node_modules",
"clean": "rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
"ci:version": "changeset version && pnpm install --frozen-lockfile false",
"coverage": "pnpm run test -- --coverage",
"prettier": "prettier --check .",

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.2'

View File

@@ -1,5 +1,11 @@
# @nhost/nextjs
## 1.13.40
### Patch Changes
- @nhost/react@2.1.1
## 1.13.39
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nextjs",
"version": "1.13.39",
"version": "1.13.40",
"description": "Nhost NextJS library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/nhost-js
## 2.2.18
### Patch Changes
- 8b127fbb6: feat: export `urlFromSubdomain` helper
## 2.2.17
### Patch Changes

View File

@@ -25,7 +25,7 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 16
version = 18
[auth]
version = '0.20.1'

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nhost-js",
"version": "2.2.17",
"version": "2.2.18",
"description": "Nhost JavaScript SDK",
"license": "MIT",
"keywords": [

View File

@@ -1,4 +1,5 @@
export * from '@nhost/hasura-auth-js'
export * from '@nhost/hasura-storage-js'
export * from './clients'
export { urlFromSubdomain } from './utils/helpers'
export * from './utils/types'

View File

@@ -5,9 +5,9 @@ export const LOCALHOST_REGEX =
/^((?<protocol>http[s]?):\/\/)?(?<host>(localhost|local))(:(?<port>(\d+|__\w+__)))?$/
/**
* `backendUrl` should now be used only when self-hosting
* `subdomain` and `region` should be used instead when using the Nhost platform
* `
* \`backendUrl\` should now be used only when self-hosting
* \`subdomain\` and `region` should be used instead when using the Nhost platform
*
* @param backendOrSubdomain
* @param service
* @returns

View File

@@ -1,5 +1,12 @@
# @nhost/react
## 2.1.1
### Patch Changes
- Updated dependencies [8b127fbb6]
- @nhost/nhost-js@2.2.18
## 2.1.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react",
"version": "2.1.0",
"version": "2.1.1",
"description": "Nhost React library",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,18 @@
# @nhost/vue
## 1.14.1
### Patch Changes
- Updated dependencies [8b127fbb6]
- @nhost/nhost-js@2.2.18
## 1.14.0
### Minor Changes
- 0c49e757c: feat: add new composable `useMultipleFilesUpload`
## 1.13.39
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/vue",
"version": "1.13.39",
"version": "1.14.1",
"description": "Nhost Vue library",
"license": "MIT",
"keywords": [

View File

@@ -1,8 +1,8 @@
export * from './client'
export * from './useAccessToken'
export * from './useAuthInterpreter'
export * from './useAuthenticated'
export * from './useAuthenticationStatus'
export * from './useAuthInterpreter'
export * from './useChangeEmail'
export * from './useChangePassword'
export * from './useConfigMfa'
@@ -10,6 +10,7 @@ export * from './useDecodedAccessToken'
export * from './useFileUpload'
export * from './useHasuraClaim'
export * from './useHasuraClaims'
export * from './useMultipleFilesUpload'
export * from './useNhostClient'
export * from './useProviderLink'
export * from './useResetPassword'

View File

@@ -8,10 +8,11 @@ import {
uploadFilePromise
} from '@nhost/nhost-js'
import { useInterpret, useSelector } from '@xstate/vue'
import { ToRefs } from 'vue'
import { InterpreterFrom } from 'xstate'
import { useNhostClient } from './useNhostClient'
export interface FileUploadComposableResult extends FileUploadState {
export interface FileUploadComposableResult extends ToRefs<FileUploadState> {
/**
* Add the file without uploading it.
*/
@@ -38,21 +39,41 @@ export type { FileItemRef }
/**
* Use the composable `useFileUploadItem` to control the file upload of a file in a multiple file upload.
*
* It has the same signature as `useFileUpload`.
*
* @example
* ```tsx
* const Item = ({itemRef}) => {
* const { name, progress} = useFileUploadItem(itemRef)
* return <li>{name} {progress}</li>
* }
* ```vue
* <!-- Parent component or page -->
*
* const List = () => {
* const { list } = useMultipleFilesUpload()
* return <ul>
* {list.map((itemRef) => <Item key={item.id} itemRef={item} />)}
* </ul>
* }
* <script lang="ts" setup>
* const { files } = useMultipleFilesUpload()
* <script lang="ts" setup>
*
* <template>
* <div v-for="(file, index) of files" :key="index">
* <FileUploadItem :file="file" />
* </div>
* </template>
*
*
* <!-- FileUploadItem component -->
*
* <script lang="ts" setup>
* import { FileItemRef } from '@nhost/nhost-js'
* import { useFileUploadItem } from '@nhost/vue'
*
* const { file } = defineProps<{ file: FileItemRef }>()
*
* const { name, progress } = useFileUploadItem(file)
* </script>
*
* <template>
* <div>
* <span>{{ name }}</span>
* <v-progress-linear v-model="progress">
* {{ progress }}
* </v-progress-linear>
* </div>
* </template>
*
* ```
*/
@@ -65,7 +86,7 @@ export const useFileUploadItem = (
ref.send({
type: 'ADD',
file: params.file,
bucketId: params.bucketId || bucketId
bucketId: params.bucketId || bucketId.value
})
}
@@ -88,14 +109,14 @@ export const useFileUploadItem = (
ref.send('DESTROY')
}
const isUploading = useSelector(ref, (state) => state.matches('uploading')).value
const isUploaded = useSelector(ref, (state) => state.matches('uploaded')).value
const isError = useSelector(ref, (state) => state.matches('error')).value
const error = useSelector(ref, (state) => state.context.error || null).value
const progress = useSelector(ref, (state) => state.context.progress).value
const id = useSelector(ref, (state) => state.context.id).value
const bucketId = useSelector(ref, (state) => state.context.bucketId).value
const name = useSelector(ref, (state) => state.context.file?.name).value
const isUploading = useSelector(ref, (state) => state.matches('uploading'))
const isUploaded = useSelector(ref, (state) => state.matches('uploaded'))
const isError = useSelector(ref, (state) => state.matches('error'))
const error = useSelector(ref, (state) => state.context.error || null)
const progress = useSelector(ref, (state) => state.context.progress)
const id = useSelector(ref, (state) => state.context.id)
const bucketId = useSelector(ref, (state) => state.context.bucketId)
const name = useSelector(ref, (state) => state.context.file?.name)
return {
add,
@@ -117,7 +138,7 @@ export const useFileUploadItem = (
* Use the composable `useFileUpload` to upload a file.
*
* @example
* ```tsx
* ```ts
* const { add,
* upload,
* cancel,

View File

@@ -0,0 +1,117 @@
import {
createMultipleFilesUploadMachine,
FileItemRef,
MultipleFilesHandlerResult,
MultipleFilesUploadState,
UploadMultipleFilesActionParams,
uploadMultipleFilesPromise
} from '@nhost/nhost-js'
import { useInterpret, useSelector } from '@xstate/vue'
import { Ref, ref, ToRefs } from 'vue'
import { useNhostClient } from './useNhostClient'
export interface MultipleFilesUploadComposableResult extends ToRefs<MultipleFilesUploadState> {
/**
* Add one or multiple files to add to the list of files to upload.
*/
add: (
params: Required<Pick<UploadMultipleFilesActionParams, 'files'>> &
UploadMultipleFilesActionParams
) => void
/**
* Upload the files that has been previously added to the list.
*/
upload: (params?: UploadMultipleFilesActionParams) => Promise<MultipleFilesHandlerResult>
/**
* Cancel the ongoing upload. The files that have been successfully uploaded will not be deleted from the server.
*/
cancel: () => void
/**
* Clear the list of files.
*/
clear: () => void
}
/**
* Use the composable `useMultipleFilesUpload` to upload multiple files.
*
* @example
* ```ts
* const {
* add,
* upload
* } = useMultipleFilesUpload()
*
* const addFiles = async (files) => {
* add({files})
* }
*
* const handleSubmit = async (e) => {
* e.preventDefault()
* await upload()
* }
* ```
*
* @docs https://docs.nhost.io/reference/vue/use-multiple-files-upload
*/
export const useMultipleFilesUpload = (): MultipleFilesUploadComposableResult => {
const { nhost } = useNhostClient()
const errors: Ref<FileItemRef[]> = ref([])
const service = useInterpret(createMultipleFilesUploadMachine, {}, (state) => {
if (state.event.type === 'UPLOAD_ERROR') {
errors.value = state.context.files.filter((ref) => ref.getSnapshot()?.context.error)
} else if (
(state.matches('uploaded') || state.event.type === 'CLEAR') &&
errors.value.length > 0
) {
errors.value = []
}
})
const add = (
params: Required<Pick<UploadMultipleFilesActionParams, 'files'>> &
UploadMultipleFilesActionParams
) => {
service.send({ type: 'ADD', ...params })
}
const upload = (params?: UploadMultipleFilesActionParams) =>
uploadMultipleFilesPromise(
{
url: nhost.storage.url,
accessToken: nhost.auth.getAccessToken(),
adminSecret: nhost.adminSecret,
...params
},
service
)
const cancel = () => {
service.send('CANCEL')
}
const clear = () => {
service.send('CLEAR')
}
const isUploading = useSelector(service, (state) => state.matches('uploading'))
const isUploaded = useSelector(service, (state) => state.matches('uploaded'))
const isError = useSelector(service, (state) => state.matches('error'))
const progress = useSelector(service, (state) => state.context.progress)
const files = useSelector(service, (state) => state.context.files)
return {
upload,
add,
clear,
cancel,
progress,
isUploaded,
isUploading,
files,
isError,
errors
}
}

2109
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,5 +5,5 @@ packages:
- 'docs'
- '!**/test/**'
- '!out/**'
- 'examples/*'
- 'examples/**'
- '!**/functions'